feat: the help is a submenu, with a contextual-help layer and a guided tour
The hamburger menu's help rows were three flat entries and a window. They are a **`Help` submenu** now, which is where every GNOME menu keeps them, with the four things a first run cannot find: - Keyboard shortcuts Ctrl+/ (the existing overview, `show_shortcuts()`) - Documentation F1 (the existing manual, `show_help()`) - Guided tour Ctrl+T (new) - Enable contextual help (new, a stateful row: its check *is* the setting) The submenu *replaces* the menu's rows rather than floating beside them, the same shape and the same reason as the plot's right-click menu: a submenu torn out of the menu it belongs to is a second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the moment anything else closed its popover. It is the pages of a `GtkStack`, not a rebuild of the box and not a swap of the popover's child: the main view holds `app.layout_toggle`, which the tick's `sync_layout_editor()` and the `L` accel read, so rebuilding would leave that pointer on a destroyed widget, and a swapped child would leave the view that is not showing without an owner. **The contextual help is one table and a "?" per surface** (`src/ui_context_help.cpp`): the plot, the trigger, the cursors, the measurements, the math channels, the signals, the event history, the time window, the status line, and the math editor's identity row. Each row names the *heading box* its button joins -- a sibling of the title, never an overlay child on a corner, which is what would cover the switch or the [edit] button already there -- so adding a surface is a row and a widget pointer. Off means *gone*, not greyed: the buttons are removed, and they are found by a data key rather than a list of pointers because the math editor's heading is built afresh on every opening and a list would go stale one dialog later. `contextual_help` and `tour_seen` are keys of `settings.toml`, written through the same `Settings`/`load_settings()`/`save_settings()` the Preferences dialog uses, whose own rows start from `load_settings()` and so carry them through untouched. **The guided tour is five steps on a `GtkOverlay`** above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover in it and therefore no pointer grab to leak -- four dim rectangles around the hole are what make it modal -- and the probe asserts that as a reading (`controllers=` against the count taken before it opened) rather than as a claim. Everything is placed from `gtk_widget_compute_bounds()` re-read on every frame, because a widget's allocation is 0 on the frame it is first shown and a window resize moves it under a one-shot measurement; the hole is kept from the last frame that could measure the target, because a frame that lands between two layout passes would otherwise empty it and make the spotlight jump. `Skip`, `Finish` and `Escape` all hide every child and leave nothing behind. A first run gets the tour once, decided by `tour_seen` and not by "the settings file did not exist" (a user who changed the theme has a file and has still never seen it), and the flag is written when the tour *starts* rather than when it finishes: a user who skips it in the first second has still seen it. `UDPSCOPE_PROBE_HELPUI_MS` drives all of it -- the auto-start, the menu's seven rows and the submenu's five, the toggle's effect read as a count of affordances on either side of it, and the tour started, stepped forwards and back and dismissed -- and the exit dump gains `help layer:` and `help figures:`, the latter being the ten surfaces' window-coordinate rectangles that the manual's annotated screenshots crop their boxes from. Two of them are what the D-Bus-free headless run can still not decide, and both are limitations rather than defects: export carries no "?" (it is a menu action with no surface of its own, and a popover inside the menu's popover is the nested-popover trap the invariants already warn about), and the tour card cannot miss its target when the target is the whole content -- which is what the plot is at the window's 756 px minimum, where the split view overlays the side panel instead of putting it beside the plot. The probe prints both rectangles rather than asserting a placement, so that case reads as the geometry it is. Also fixed while it was found: `gtk_widget_measure()` includes the widget's own margins (measured: a 280 px box with `margin-start 453` reports 733), so a measure that is used to *place* the widget is a feedback loop -- that is what made the tour card walk between the corner and the target from frame to frame.
This commit is contained in:
1 parent
c1460a6977
commit
1baa58b8fd
20 files changed
+1558
-30
No files matched your search
+417
@@ -0,0 +1,417 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
//
|
||||
// The guided tour: a walk over the surfaces a first run has no way of knowing about, drawn on a
|
||||
// `GtkOverlay` inserted above the window's content.
|
||||
//
|
||||
// Why an overlay and not a popover per step: a popover takes a pointer grab, and a run of them takes
|
||||
// one per step. The overlay takes none at all -- what makes the tour modal is the four dim
|
||||
// rectangles, which are ordinary widgets that swallow the clicks that land on them, and dismissing
|
||||
// the tour is hiding them. There is no grab to leak and nothing to unparent.
|
||||
//
|
||||
// The spotlight is *four* rectangles and not a drawing: the top, bottom, left and right strips
|
||||
// around the target, positioned by margins each frame from `gtk_widget_compute_bounds()`. That is
|
||||
// the whole of "dims everything except the target", it needs no cairo and no paint function, and it
|
||||
// is re-measured on every frame -- the target's own allocation is 0 on the frame it is first shown
|
||||
// (see the allocation bullets in `docs/claude/invariants-trigger-and-pinning.md`), and a window
|
||||
// resize moves it under a spotlight that a one-shot measurement would leave behind.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
namespace {
|
||||
|
||||
/// One step. The target is measured, never named by a stored rectangle: the point of re-measuring
|
||||
/// every frame is that nothing here knows where the widget is.
|
||||
struct TourStep {
|
||||
const char* id;
|
||||
const char* title;
|
||||
const char* body;
|
||||
GtkWidget* App::* target;
|
||||
};
|
||||
|
||||
constexpr TourStep kTourSteps[] = {
|
||||
{"plot", "The plot",
|
||||
"Every signal you have switched on is drawn here, all rows sharing one time axis. The wheel "
|
||||
"zooms, a drag pans, and a right-click on a row opens that plot's own menu.",
|
||||
&App::area_overlay},
|
||||
{"side", "The side panel",
|
||||
"The working surface beside the plot. Measure holds the cursors and the measurements; Signals "
|
||||
"holds the trigger and the list of signals. The buttons in the bar above show and hide it.",
|
||||
&App::side_box},
|
||||
{"trigger", "The trigger",
|
||||
"Describe an event -- a signal, a condition, a level and the window around it -- and arm it: "
|
||||
"the view then freezes on the first event that matches, however rare it is.",
|
||||
&App::trigger_body},
|
||||
{"signals", "The signals",
|
||||
"Every signal the source declares, each with the switch that acquires it. A signal has to be "
|
||||
"switched on before it can be plotted, measured or searched.",
|
||||
&App::signal_list},
|
||||
{"status", "The status line",
|
||||
"The stream's own report, once a second: the display mode, the rate arriving, the window on "
|
||||
"screen and the buffer holding it. The information button opens the counters behind it.",
|
||||
&App::status_row},
|
||||
};
|
||||
constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0]));
|
||||
|
||||
/// The card's width. Wide enough for a comfortable line of the paragraph, narrow enough to fit
|
||||
/// beside the plot on the documented window -- at 756 px the side panel is 320 and a card wider than
|
||||
/// that is a card that has to cover its own target. It is a *floor* on the box, not a cap on the
|
||||
/// paragraph: the wrapped label is capped by `max_width_chars` so the natural width never exceeds it.
|
||||
constexpr int kCardWidth = 280;
|
||||
/// The gap between the hole and whatever is placed beside it. Eight and not twelve: the side panel
|
||||
/// is 320 px, and it has to hold the card and both gaps.
|
||||
constexpr int kGap = 8;
|
||||
|
||||
/// Places an overlay child of the tour overlay at an exact rectangle. `GtkOverlay` positions an
|
||||
/// overlay child by its alignment and its margins and gives it its measured size, so a fixed
|
||||
/// rectangle is a size request plus two margins.
|
||||
void place(GtkWidget* widget, int x, int y, int width, int height) {
|
||||
gtk_widget_set_halign(widget, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(widget, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_start(widget, std::max(0, x));
|
||||
gtk_widget_set_margin_top(widget, std::max(0, y));
|
||||
gtk_widget_set_size_request(widget, std::max(0, width), std::max(0, height));
|
||||
}
|
||||
|
||||
/// Places an overlay child by its *natural* size at a point. The card sizes itself -- its paragraph
|
||||
/// wraps at a stated width -- and only its position is ours. Giving it the width we measured back as
|
||||
/// a size request is a feedback loop: a request is a floor on the widget's own measure, so the width
|
||||
/// it was placed at becomes the width it reports next frame, and a starting value that is one pixel
|
||||
/// too large grows by that pixel for ever.
|
||||
void place_natural(GtkWidget* widget, int x, int y) {
|
||||
gtk_widget_set_halign(widget, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(widget, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_start(widget, std::max(0, x));
|
||||
gtk_widget_set_margin_top(widget, std::max(0, y));
|
||||
}
|
||||
|
||||
/// The step currently walked, or nullptr when the tour is not on a step.
|
||||
const TourStep* current_step(const App& app) {
|
||||
if (app.tour_index < 0 || app.tour_index >= static_cast<int>(app.tour_plan.size())) return nullptr;
|
||||
return &kTourSteps[app.tour_plan[static_cast<std::size_t>(app.tour_index)]];
|
||||
}
|
||||
|
||||
/// Restates the card's text for the current step. Called on a change of step, not per frame.
|
||||
void show_step(App& app) {
|
||||
const TourStep* step = current_step(app);
|
||||
if (step == nullptr || app.tour_card == nullptr) return;
|
||||
// The hole belongs to the step: reusing the last one for a frame would put the spotlight on the
|
||||
// surface the tour has just left, and the next frame fills this one in.
|
||||
app.tour_hole[0] = app.tour_hole[1] = app.tour_hole[2] = app.tour_hole[3] = 0;
|
||||
char markup[128];
|
||||
std::snprintf(markup, sizeof(markup), "<b>%s</b>", step->title);
|
||||
gtk_label_set_markup(GTK_LABEL(app.tour_title), markup);
|
||||
gtk_label_set_text(GTK_LABEL(app.tour_body), step->body);
|
||||
char counter[64];
|
||||
std::snprintf(counter, sizeof(counter), "%d of %d",
|
||||
app.tour_index + 1, static_cast<int>(app.tour_plan.size()));
|
||||
gtk_label_set_text(GTK_LABEL(app.tour_counter), counter);
|
||||
gtk_widget_set_visible(app.tour_back, app.tour_index > 0);
|
||||
gtk_button_set_label(GTK_BUTTON(app.tour_next),
|
||||
app.tour_index + 1 >= static_cast<int>(app.tour_plan.size()) ? "Finish"
|
||||
: "Next");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GtkWidget* tour_target(App& app) {
|
||||
const TourStep* step = current_step(app);
|
||||
return step != nullptr ? app.*(step->target) : nullptr;
|
||||
}
|
||||
|
||||
void build_tour(App& app) {
|
||||
if (app.tour_overlay == nullptr) return;
|
||||
for (GtkWidget*& dim : app.tour_dim) {
|
||||
dim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(dim, "udpscope-tour-dim");
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), dim);
|
||||
}
|
||||
app.tour_spot = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(app.tour_spot, "udpscope-tour-spot");
|
||||
// The outline is a frame, not a shield: a click on the *target* is a click on the target.
|
||||
gtk_widget_set_can_target(app.tour_spot, FALSE);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), app.tour_spot);
|
||||
|
||||
// The card, last: overlay children are painted in the order they were added, and the card has to
|
||||
// be above the dim strips that cover the rest of the window.
|
||||
app.tour_card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_add_css_class(app.tour_card, "udpscope-tour-card");
|
||||
gtk_widget_set_size_request(app.tour_card, kCardWidth, -1);
|
||||
|
||||
app.tour_title = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(app.tour_title), 0.0f);
|
||||
gtk_box_append(GTK_BOX(app.tour_card), app.tour_title);
|
||||
|
||||
app.tour_body = gtk_label_new(nullptr);
|
||||
gtk_label_set_wrap(GTK_LABEL(app.tour_body), TRUE);
|
||||
// The cap that keeps the card narrow. `max_width_chars` is what bounds a wrapped label's *natural*
|
||||
// width; without it the paragraph's natural width is the whole sentence on one line and the card
|
||||
// is that wide however it is placed.
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(app.tour_body), 30);
|
||||
gtk_label_set_xalign(GTK_LABEL(app.tour_body), 0.0f);
|
||||
gtk_box_append(GTK_BOX(app.tour_card), app.tour_body);
|
||||
|
||||
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.tour_counter = gtk_label_new(nullptr);
|
||||
gtk_widget_add_css_class(app.tour_counter, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(app.tour_counter), 0.0f);
|
||||
gtk_widget_set_hexpand(app.tour_counter, TRUE);
|
||||
gtk_box_append(GTK_BOX(footer), app.tour_counter);
|
||||
app.tour_back = gtk_button_new_with_label("Back");
|
||||
g_signal_connect(app.tour_back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
tour_step(*static_cast<App*>(user), -1);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(footer), app.tour_back);
|
||||
GtkWidget* skip = gtk_button_new_with_label("Skip");
|
||||
g_signal_connect(skip, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
tour_dismiss(*static_cast<App*>(user));
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(footer), skip);
|
||||
app.tour_next = gtk_button_new_with_label("Next");
|
||||
gtk_widget_add_css_class(app.tour_next, "suggested-action");
|
||||
g_signal_connect(app.tour_next, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
tour_step(*static_cast<App*>(user), 1);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(footer), app.tour_next);
|
||||
gtk_box_append(GTK_BOX(app.tour_card), footer);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), app.tour_card);
|
||||
|
||||
// Everything starts hidden: the tour is not on screen until it is started.
|
||||
for (GtkWidget* dim : app.tour_dim) gtk_widget_set_visible(dim, FALSE);
|
||||
gtk_widget_set_visible(app.tour_spot, FALSE);
|
||||
gtk_widget_set_visible(app.tour_card, FALSE);
|
||||
}
|
||||
|
||||
void start_tour(App& app) {
|
||||
if (app.tour_overlay == nullptr || app.tour_card == nullptr) return;
|
||||
// "The tour has run" is written when it *starts*: a user who skips it has still seen it, and the
|
||||
// alternative -- marking it on the last step -- would offer the whole tour again to everyone who
|
||||
// decided in the first second that they did not want it.
|
||||
if (!app.settings.tour_seen) {
|
||||
app.settings.tour_seen = true;
|
||||
(void)save_settings(app.settings);
|
||||
}
|
||||
// The steps whose target is on screen, in table order. A surface that is not there is not walked
|
||||
// over -- the point of asking `is_visible()` rather than `get_visible()` is that a widget under a
|
||||
// hidden parent still reports its own flag as true.
|
||||
app.tour_plan.clear();
|
||||
for (int i = 0; i < kTourStepCount; ++i) {
|
||||
GtkWidget* target = app.*(kTourSteps[i].target);
|
||||
if (target != nullptr && gtk_widget_is_visible(target)) app.tour_plan.push_back(i);
|
||||
}
|
||||
if (app.tour_plan.empty()) return;
|
||||
app.tour_index = 0;
|
||||
app.tour_running = true;
|
||||
for (GtkWidget* dim : app.tour_dim) gtk_widget_set_visible(dim, TRUE);
|
||||
gtk_widget_set_visible(app.tour_spot, TRUE);
|
||||
gtk_widget_set_visible(app.tour_card, TRUE);
|
||||
show_step(app);
|
||||
update_tour(app);
|
||||
}
|
||||
|
||||
void tour_step(App& app, int delta) {
|
||||
if (!app.tour_running) return;
|
||||
const int next = app.tour_index + delta;
|
||||
if (next < 0 || next >= static_cast<int>(app.tour_plan.size())) {
|
||||
tour_dismiss(app);
|
||||
return;
|
||||
}
|
||||
app.tour_index = next;
|
||||
show_step(app);
|
||||
update_tour(app);
|
||||
}
|
||||
|
||||
void tour_dismiss(App& app) {
|
||||
app.tour_running = false;
|
||||
app.tour_index = 0;
|
||||
for (GtkWidget* dim : app.tour_dim) {
|
||||
if (dim != nullptr) gtk_widget_set_visible(dim, FALSE);
|
||||
}
|
||||
if (app.tour_spot != nullptr) gtk_widget_set_visible(app.tour_spot, FALSE);
|
||||
if (app.tour_card != nullptr) gtk_widget_set_visible(app.tour_card, FALSE);
|
||||
}
|
||||
|
||||
bool tour_active(const App& app) { return app.tour_running; }
|
||||
|
||||
int tour_step_count(const App& app) { return static_cast<int>(app.tour_plan.size()); }
|
||||
|
||||
int tour_step_index(const App& app) { return app.tour_index; }
|
||||
|
||||
void update_tour(App& app) {
|
||||
if (!app.tour_running || app.tour_overlay == nullptr) return;
|
||||
const int w = gtk_widget_get_width(app.tour_overlay);
|
||||
const int h = gtk_widget_get_height(app.tour_overlay);
|
||||
if (w <= 0 || h <= 0) return; // no allocation yet: nothing to place anything against
|
||||
|
||||
// The target, re-measured every frame. Its allocation is 0 on the frame it is first shown, so a
|
||||
// one-shot measurement would leave the hole in the corner.
|
||||
GtkWidget* target = tour_target(app);
|
||||
graphene_rect_t bounds = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
const bool target_present = target != nullptr && gtk_widget_is_visible(target);
|
||||
if (target_present && gtk_widget_compute_bounds(target, app.tour_overlay, &bounds)) {
|
||||
// The hole: the target, grown by three pixels so the outline sits *around* the widget rather than
|
||||
// on it, and clamped to the overlay -- a target partly off screen is an ordinary case (a sidebar
|
||||
// dragged wide), and a negative rectangle is not something a margin can express.
|
||||
const int x0 = static_cast<int>(std::floor(bounds.origin.x)) - 3;
|
||||
const int y0 = static_cast<int>(std::floor(bounds.origin.y)) - 3;
|
||||
const int x1 = static_cast<int>(std::ceil(bounds.origin.x + bounds.size.width)) + 3;
|
||||
const int y1 = static_cast<int>(std::ceil(bounds.origin.y + bounds.size.height)) + 3;
|
||||
app.tour_hole[0] = std::clamp(x0, 0, w);
|
||||
app.tour_hole[1] = std::clamp(y0, 0, h);
|
||||
app.tour_hole[2] = std::clamp(x1, 0, w) - app.tour_hole[0];
|
||||
app.tour_hole[3] = std::clamp(y1, 0, h) - app.tour_hole[1];
|
||||
} else if (!target_present) {
|
||||
// Nothing on screen to point at: the whole surface dims and the card takes a corner. A step whose
|
||||
// target went away mid-tour is not a step to be skipped silently.
|
||||
app.tour_hole[0] = app.tour_hole[1] = app.tour_hole[2] = app.tour_hole[3] = 0;
|
||||
}
|
||||
// ... and when the target *is* there but could not be measured, the previous hole stands: a frame
|
||||
// can arrive between two layout passes, and emptying the hole there is the spotlight and the card
|
||||
// jumping to the corner and back.
|
||||
const int hx = app.tour_hole[0];
|
||||
const int hy = app.tour_hole[1];
|
||||
const int hw = app.tour_hole[2];
|
||||
const int hh = app.tour_hole[3];
|
||||
|
||||
place(app.tour_dim[0], 0, 0, w, hy); // above the hole
|
||||
place(app.tour_dim[1], 0, hy + hh, w, h - hy - hh); // below it
|
||||
place(app.tour_dim[2], 0, hy, hx, hh); // to its left
|
||||
place(app.tour_dim[3], hx + hw, hy, w - hx - hw, hh); // to its right
|
||||
place(app.tour_spot, hx, hy, hw, hh);
|
||||
|
||||
// The card, placed clear of the hole whenever the window has room for it. The strip order is the
|
||||
// order a card is easiest to read in: under the thing it describes, then over it, then in
|
||||
// whichever column is beside it -- which is, on this window, the side panel for the plot's step and
|
||||
// the plot for every step in the side panel.
|
||||
//
|
||||
// Its size is measured, never requested -- but `gtk_widget_measure()` reports the size *including
|
||||
// the widget's own margins* (measured: a 280 px box with `margin-start` 453 measures 733). Ours are
|
||||
// the card's position, so reading the measure without taking them off is a feedback loop: the width
|
||||
// it was placed at becomes the width it reports, and the card walks to the far corner and back
|
||||
// between frames.
|
||||
int cw = 0;
|
||||
int ch = 0;
|
||||
gtk_widget_measure(app.tour_card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr, nullptr);
|
||||
cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(app.tour_card) -
|
||||
gtk_widget_get_margin_end(app.tour_card));
|
||||
gtk_widget_measure(app.tour_card, GTK_ORIENTATION_VERTICAL, cw, nullptr, &ch, nullptr, nullptr);
|
||||
ch = std::max(0, ch - gtk_widget_get_margin_top(app.tour_card) -
|
||||
gtk_widget_get_margin_bottom(app.tour_card));
|
||||
int cx = 0, cy = 0;
|
||||
if (h - (hy + hh) >= ch + 2 * kGap) {
|
||||
cx = std::clamp(hx, kGap, std::max(kGap, w - cw - kGap));
|
||||
cy = hy + hh + kGap;
|
||||
} else if (hy >= ch + 2 * kGap) {
|
||||
cx = std::clamp(hx, kGap, std::max(kGap, w - cw - kGap));
|
||||
cy = hy - ch - kGap;
|
||||
} else if (w - (hx + hw) >= cw + 2 * kGap) {
|
||||
cx = hx + hw + kGap;
|
||||
cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap));
|
||||
} else if (hx >= cw + 2 * kGap) {
|
||||
cx = hx - cw - kGap;
|
||||
cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap));
|
||||
} else {
|
||||
// No clear side at all -- the target is nearly the whole window, which is the plot's step in a
|
||||
// window narrow enough for the split view to *overlay* the side panel rather than put it beside
|
||||
// the plot. The card goes to the bottom-right corner, which is where the least of the target is,
|
||||
// and the two readings the probe prints (`spot=`, `card=`) are what show how little choice there
|
||||
// was rather than the card having been placed badly.
|
||||
cx = std::max(kGap, w - cw - kGap);
|
||||
cy = std::max(kGap, h - ch - kGap);
|
||||
}
|
||||
place_natural(app.tour_card, cx, cy);
|
||||
}
|
||||
|
||||
gboolean tour_autostart(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
app->tour_auto_started = true;
|
||||
start_tour(*app);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// --- the dump's readings ----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
/// The surfaces the help's annotated figures label. Their geometry is what a figure's crop box is
|
||||
/// drawn from, and it has to be *measured* -- at 1280x800 the panels are not where a plausible guess
|
||||
/// puts them, and a box drawn from a guess is a box over the wrong thing.
|
||||
struct FigureSurface {
|
||||
const char* name;
|
||||
GtkWidget* App::* widget;
|
||||
};
|
||||
|
||||
constexpr FigureSurface kFigureSurfaces[] = {
|
||||
{"trigger_head", &App::trigger_head},
|
||||
{"trigger_summary", &App::trigger_summary},
|
||||
{"trigger_panel", &App::trigger_panel},
|
||||
{"signal_list", &App::signal_list},
|
||||
{"side_switcher", &App::side_switcher},
|
||||
{"side_stack", &App::side_stack},
|
||||
{"window_button", &App::window_button},
|
||||
{"status_label", &App::status_label},
|
||||
{"header_bar", &App::toolbar},
|
||||
{"plot", &App::area_overlay},
|
||||
};
|
||||
constexpr int kFigureSurfaceCount =
|
||||
static_cast<int>(sizeof(kFigureSurfaces) / sizeof(kFigureSurfaces[0]));
|
||||
|
||||
} // namespace
|
||||
|
||||
void capture_help_state(App& app) {
|
||||
std::string ids;
|
||||
const int hints = context_help_count(app, &ids);
|
||||
// The icon is read back through the theme for the reason the pane [edit] buttons are: a name
|
||||
// Adwaita does not have is a *blank* control that works and reads as nothing at all.
|
||||
GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
|
||||
const bool icon_ok = theme != nullptr && gtk_icon_theme_has_icon(theme, "dialog-question-symbolic");
|
||||
char layer[512];
|
||||
std::snprintf(layer, sizeof(layer),
|
||||
"contextual=%s hints=%d ids={%s} icon=dialog-question-symbolic(%s); tour=%s steps=%d "
|
||||
"step=%d auto=%d",
|
||||
app.settings.contextual_help ? "on" : "off", hints, ids.empty() ? "" : ids.c_str(),
|
||||
icon_ok ? "ok" : "MISSING", app.tour_running ? "running" : "idle",
|
||||
tour_step_count(app), tour_step_index(app), app.tour_auto_started ? 1 : 0);
|
||||
app.dbg_context_help = layer;
|
||||
app.dbg_tour_steps = tour_step_count(app);
|
||||
app.dbg_tour_step = tour_step_index(app);
|
||||
app.dbg_tour_visible = app.tour_running ? 1 : 0;
|
||||
|
||||
std::string figures;
|
||||
for (int i = 0; i < kFigureSurfaceCount; ++i) {
|
||||
const FigureSurface& surface = kFigureSurfaces[i];
|
||||
GtkWidget* widget = app.*(surface.widget);
|
||||
graphene_rect_t r = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
int x = -1, y = -1, width = 0, height = 0;
|
||||
// `is_visible()`, not `get_visible()`: a widget under a hidden parent -- the trigger summary
|
||||
// while the pane is in configuration mode, the Measure page while the Signals tab is showing --
|
||||
// keeps its own flag true and reads 0x0, which a crop box would take for a real rectangle.
|
||||
if (widget != nullptr && gtk_widget_is_visible(widget) &&
|
||||
gtk_widget_compute_bounds(widget, app.window, &r)) {
|
||||
x = static_cast<int>(std::lround(r.origin.x));
|
||||
y = static_cast<int>(std::lround(r.origin.y));
|
||||
width = static_cast<int>(std::lround(r.size.width));
|
||||
height = static_cast<int>(std::lround(r.size.height));
|
||||
}
|
||||
if (!figures.empty()) figures.push_back(' ');
|
||||
figures += std::string(surface.name) + "=" + std::to_string(x) + "," + std::to_string(y) + "," +
|
||||
std::to_string(width) + "," + std::to_string(height);
|
||||
}
|
||||
app.dbg_help_figures = figures;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user