help: a tinier (i), hints beside their controls, and the tour starting in the chooser
The contextual help becomes the affordance the ask names: a plain (i) (`dialog-information-symbolic`) at **16x16**, and one that sits beside the control it explains rather than only in a pane's heading. - The mark: `GtkMenuButton` ignores `min-width` on the widget and a size request, so the size is made in `install_css()` on all three nodes (outer `menubutton`, its `button` subnode, the glyph). Measured 16x16 against the default 34x34; the dump prints it as `hint=WxH`, measured with `gtk_widget_compute_bounds()` on the first *visible* hint. - More sites: `events_record` (the "record events" switch in the trigger panel), `trigger_advanced` (the pre-trigger share row), `signals_config` (the signals pane's hint line, wrapped in a box so a hint can join it). Hints join as siblings, so none can cover a switch, pencil or dropdown. - The source chooser gets its own layer -- `chooser_sources` (a new heading above the saved-source list) and `chooser_waveform` (the waveforms action row) -- built with the one `context_help_button()` both windows use and re-synced by its Help menu's toggle. The form's `AdwEntryRow`s cannot host a sibling hint, so none is invented for the host/port fields. - The tour is one implementation over two windows: `ui_impl::TourView` plus a list of `TourStop`s; each window keeps its own table. A walk that passes its last stop calls `on_done`, and the chooser's sets `Config::tour_pending`, so a first run is one tour that crosses from the chooser into the app. The chooser's tour auto-starts from `settings.tour_seen` exactly as the window's does, and its "Guided tour" row now starts *it* instead of only requesting the window's. - Probes: `UDPSCOPE_PROBE_CHOOSER_TOUR=1` / `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`, the hint size in the `help layer:`/`[helpui] contextual on:` lines, and the chooser's hints in its `chooser ui:` line. Docs updated in invariants-gtk-and-chrome.md, probe-readings.md and verification.md.
This commit is contained in:
1 parent
395c9caec3
commit
829176248a
14 files changed
+911
-303
No files matched your search
+237
-184
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
// `GtkOverlay` inserted above a 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
|
||||
@@ -15,6 +15,13 @@
|
||||
// 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.
|
||||
//
|
||||
// There are *two* tours and one implementation. The main window's walks the plot, the side panel,
|
||||
// the trigger and the like; the source chooser's -- which appears *before* `App` exists -- walks the
|
||||
// chooser's own widgets and then hands the tour on to the window through `Config::tour_pending`. The
|
||||
// machinery below is written against a `TourView` and a list of `TourStop`s, so the difference
|
||||
// between the two is only which widgets they name: the chooser keeps its table in
|
||||
// `source_dialog.cpp`, where its widgets live.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
|
||||
@@ -24,44 +31,13 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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
|
||||
@@ -94,173 +70,166 @@ void place_natural(GtkWidget* widget, int x, int y) {
|
||||
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)]];
|
||||
/// The stop currently walked, or nullptr when the walk is not on a stop.
|
||||
const TourStop* current_stop(const TourView& view) {
|
||||
if (view.index < 0 || view.index >= static_cast<int>(view.plan.size())) return nullptr;
|
||||
return &view.plan[static_cast<std::size_t>(view.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;
|
||||
/// Restates the card's text for the current stop. Called on a change of step, not per frame.
|
||||
void show_step(TourView& view) {
|
||||
const TourStop* stop = current_stop(view);
|
||||
if (stop == nullptr || view.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;
|
||||
view.hole[0] = view.hole[1] = view.hole[2] = view.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);
|
||||
std::snprintf(markup, sizeof(markup), "<b>%s</b>", stop->title);
|
||||
gtk_label_set_markup(GTK_LABEL(view.title), markup);
|
||||
gtk_label_set_text(GTK_LABEL(view.body), stop->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");
|
||||
std::snprintf(counter, sizeof(counter), "%d of %d", view.index + 1,
|
||||
static_cast<int>(view.plan.size()));
|
||||
gtk_label_set_text(GTK_LABEL(view.counter), counter);
|
||||
gtk_widget_set_visible(view.back, view.index > 0);
|
||||
gtk_button_set_label(GTK_BUTTON(view.next),
|
||||
view.index + 1 >= static_cast<int>(view.plan.size()) ? "Finish" : "Next");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
GtkWidget* tour_target(App& app) {
|
||||
const TourStep* step = current_step(app);
|
||||
return step != nullptr ? app.*(step->target) : nullptr;
|
||||
}
|
||||
// --- the machinery, shared by both windows ----------------------------------
|
||||
|
||||
void build_tour(App& app) {
|
||||
if (app.tour_overlay == nullptr) return;
|
||||
for (GtkWidget*& dim : app.tour_dim) {
|
||||
void tour_build(TourView& view, GtkWidget* overlay) {
|
||||
if (overlay == nullptr || view.card != nullptr) return;
|
||||
view.overlay = overlay;
|
||||
for (GtkWidget*& dim : view.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);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), dim);
|
||||
}
|
||||
app.tour_spot = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(app.tour_spot, "udpscope-tour-spot");
|
||||
view.spot = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(view.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);
|
||||
gtk_widget_set_can_target(view.spot, FALSE);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), view.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);
|
||||
view.card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_add_css_class(view.card, "udpscope-tour-card");
|
||||
gtk_widget_set_size_request(view.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);
|
||||
view.title = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(view.title), 0.0f);
|
||||
gtk_box_append(GTK_BOX(view.card), view.title);
|
||||
|
||||
app.tour_body = gtk_label_new(nullptr);
|
||||
gtk_label_set_wrap(GTK_LABEL(app.tour_body), TRUE);
|
||||
view.body = gtk_label_new(nullptr);
|
||||
gtk_label_set_wrap(GTK_LABEL(view.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);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(view.body), 30);
|
||||
gtk_label_set_xalign(GTK_LABEL(view.body), 0.0f);
|
||||
gtk_box_append(GTK_BOX(view.card), view.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);
|
||||
view.counter = gtk_label_new(nullptr);
|
||||
gtk_widget_add_css_class(view.counter, "dim-label");
|
||||
gtk_label_set_xalign(GTK_LABEL(view.counter), 0.0f);
|
||||
gtk_widget_set_hexpand(view.counter, TRUE);
|
||||
gtk_box_append(GTK_BOX(footer), view.counter);
|
||||
view.back = gtk_button_new_with_label("Back");
|
||||
g_signal_connect(view.back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
tour_advance(*static_cast<TourView*>(user), -1);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(footer), app.tour_back);
|
||||
&view);
|
||||
gtk_box_append(GTK_BOX(footer), view.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));
|
||||
tour_end(*static_cast<TourView*>(user));
|
||||
}),
|
||||
&app);
|
||||
&view);
|
||||
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);
|
||||
view.next = gtk_button_new_with_label("Next");
|
||||
gtk_widget_add_css_class(view.next, "suggested-action");
|
||||
g_signal_connect(view.next, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
tour_advance(*static_cast<TourView*>(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);
|
||||
&view);
|
||||
gtk_box_append(GTK_BOX(footer), view.next);
|
||||
gtk_box_append(GTK_BOX(view.card), footer);
|
||||
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), view.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);
|
||||
for (GtkWidget* dim : view.dim) gtk_widget_set_visible(dim, FALSE);
|
||||
gtk_widget_set_visible(view.spot, FALSE);
|
||||
gtk_widget_set_visible(view.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_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
|
||||
void* done_user) {
|
||||
if (view.overlay == nullptr || view.card == nullptr) return;
|
||||
view.plan = std::move(stops);
|
||||
if (view.plan.empty()) return;
|
||||
view.on_done = on_done;
|
||||
view.done_user = done_user;
|
||||
view.index = 0;
|
||||
view.running = true;
|
||||
for (GtkWidget* dim : view.dim) gtk_widget_set_visible(dim, TRUE);
|
||||
gtk_widget_set_visible(view.spot, TRUE);
|
||||
gtk_widget_set_visible(view.card, TRUE);
|
||||
show_step(view);
|
||||
tour_update(view);
|
||||
}
|
||||
|
||||
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);
|
||||
void tour_advance(TourView& view, int delta) {
|
||||
if (!view.running) return;
|
||||
const int next = view.index + delta;
|
||||
if (next < 0 || next >= static_cast<int>(view.plan.size())) {
|
||||
// Past the last stop: this is the walk *finishing*, which is the one ending that hands the tour
|
||||
// on. Take the callback out before `tour_end()` clears it.
|
||||
void (*done)(void*) = view.on_done;
|
||||
void* user = view.done_user;
|
||||
tour_end(view);
|
||||
if (done != nullptr) done(user);
|
||||
return;
|
||||
}
|
||||
app.tour_index = next;
|
||||
show_step(app);
|
||||
update_tour(app);
|
||||
view.index = next;
|
||||
show_step(view);
|
||||
tour_update(view);
|
||||
}
|
||||
|
||||
void tour_dismiss(App& app) {
|
||||
app.tour_running = false;
|
||||
app.tour_index = 0;
|
||||
for (GtkWidget* dim : app.tour_dim) {
|
||||
void tour_end(TourView& view) {
|
||||
view.running = false;
|
||||
view.index = 0;
|
||||
view.plan.clear();
|
||||
view.on_done = nullptr;
|
||||
view.done_user = nullptr;
|
||||
for (GtkWidget* dim : view.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);
|
||||
if (view.spot != nullptr) gtk_widget_set_visible(view.spot, FALSE);
|
||||
if (view.card != nullptr) gtk_widget_set_visible(view.card, FALSE);
|
||||
}
|
||||
|
||||
bool tour_active(const App& app) { return app.tour_running; }
|
||||
GtkWidget* tour_target(TourView& view) {
|
||||
const TourStop* stop = current_stop(view);
|
||||
return stop != nullptr ? stop->target : nullptr;
|
||||
}
|
||||
|
||||
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);
|
||||
void tour_update(TourView& view) {
|
||||
if (!view.running || view.overlay == nullptr) return;
|
||||
const int w = gtk_widget_get_width(view.overlay);
|
||||
const int h = gtk_widget_get_height(view.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);
|
||||
GtkWidget* target = tour_target(view);
|
||||
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)) {
|
||||
if (target_present && gtk_widget_compute_bounds(target, view.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.
|
||||
@@ -268,28 +237,28 @@ void update_tour(App& app) {
|
||||
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];
|
||||
view.hole[0] = std::clamp(x0, 0, w);
|
||||
view.hole[1] = std::clamp(y0, 0, h);
|
||||
view.hole[2] = std::clamp(x1, 0, w) - view.hole[0];
|
||||
view.hole[3] = std::clamp(y1, 0, h) - view.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;
|
||||
view.hole[0] = view.hole[1] = view.hole[2] = view.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];
|
||||
const int hx = view.hole[0];
|
||||
const int hy = view.hole[1];
|
||||
const int hw = view.hole[2];
|
||||
const int hh = view.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);
|
||||
place(view.dim[0], 0, 0, w, hy); // above the hole
|
||||
place(view.dim[1], 0, hy + hh, w, h - hy - hh); // below it
|
||||
place(view.dim[2], 0, hy, hx, hh); // to its left
|
||||
place(view.dim[3], hx + hw, hy, w - hx - hw, hh); // to its right
|
||||
place(view.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
|
||||
@@ -316,19 +285,18 @@ void update_tour(App& app) {
|
||||
int cw = 0;
|
||||
int ch = 0;
|
||||
graphene_rect_t card_bounds = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||
if (gtk_widget_is_visible(app.tour_card) && gtk_widget_get_width(app.tour_card) > 0 &&
|
||||
gtk_widget_compute_bounds(app.tour_card, app.tour_overlay, &card_bounds)) {
|
||||
if (gtk_widget_is_visible(view.card) && gtk_widget_get_width(view.card) > 0 &&
|
||||
gtk_widget_compute_bounds(view.card, view.overlay, &card_bounds)) {
|
||||
cw = static_cast<int>(std::ceil(card_bounds.size.width));
|
||||
ch = static_cast<int>(std::ceil(card_bounds.size.height));
|
||||
}
|
||||
if (cw <= 0 || 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, -1, 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));
|
||||
gtk_widget_measure(view.card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr, nullptr);
|
||||
cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(view.card) -
|
||||
gtk_widget_get_margin_end(view.card));
|
||||
gtk_widget_measure(view.card, GTK_ORIENTATION_VERTICAL, -1, nullptr, &ch, nullptr, nullptr);
|
||||
ch = std::max(0, ch - gtk_widget_get_margin_top(view.card) -
|
||||
gtk_widget_get_margin_bottom(view.card));
|
||||
}
|
||||
int cx = 0, cy = 0;
|
||||
if (h - (hy + hh) >= ch + 2 * kGap) {
|
||||
@@ -352,12 +320,92 @@ void update_tour(App& app) {
|
||||
cx = std::max(kGap, w - cw - kGap);
|
||||
cy = std::max(kGap, h - ch - kGap);
|
||||
}
|
||||
place_natural(app.tour_card, cx, cy);
|
||||
place_natural(view.card, cx, cy);
|
||||
}
|
||||
|
||||
// --- the main window's tour, and its table of stops --------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
/// One stop of the *window's* tour. 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]));
|
||||
|
||||
} // namespace
|
||||
|
||||
void build_tour(App& app) {
|
||||
tour_build(app.tour, app.tour.overlay);
|
||||
}
|
||||
|
||||
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 stops 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.
|
||||
std::vector<TourStop> stops;
|
||||
for (int i = 0; i < kTourStepCount; ++i) {
|
||||
GtkWidget* target = app.*(kTourSteps[i].target);
|
||||
if (target != nullptr && gtk_widget_is_visible(target)) {
|
||||
stops.push_back({target, kTourSteps[i].title, kTourSteps[i].body});
|
||||
}
|
||||
}
|
||||
if (stops.empty()) return;
|
||||
tour_begin(app.tour, std::move(stops), nullptr, nullptr);
|
||||
}
|
||||
|
||||
void tour_step(App& app, int delta) { tour_advance(app.tour, delta); }
|
||||
|
||||
void tour_dismiss(App& app) { tour_end(app.tour); }
|
||||
|
||||
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) { tour_update(app.tour); }
|
||||
|
||||
GtkWidget* tour_target(App& app) { return tour_target(app.tour); }
|
||||
|
||||
gboolean tour_autostart(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
if (app->window == nullptr || app->tour.overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
app->tour_auto_started = true;
|
||||
start_tour(*app);
|
||||
return G_SOURCE_REMOVE;
|
||||
@@ -365,7 +413,7 @@ gboolean tour_autostart(gpointer user) {
|
||||
|
||||
gboolean tour_start_requested(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
if (app->window == nullptr || app->tour.overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
// Not `tour_auto_started`: the chooser's entry is the user asking for the tour before the window
|
||||
// existed, and the reading has to say which of the two started it.
|
||||
app->tour_requested = true;
|
||||
@@ -406,21 +454,26 @@ 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.
|
||||
// Adwaita does not have is a *blank* control that works and reads as nothing at all. The size is
|
||||
// the reading a "much tinier" affordance is checked against, and it is measured the way every
|
||||
// other rectangle here is (`gtk_widget_compute_bounds()`), on the tick while the widget is alive.
|
||||
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];
|
||||
const bool icon_ok = theme != nullptr && gtk_icon_theme_has_icon(theme, "dialog-information-symbolic");
|
||||
int hint_w = 0;
|
||||
int hint_h = 0;
|
||||
context_help_size(app, &hint_w, &hint_h);
|
||||
char layer[640];
|
||||
std::snprintf(layer, sizeof(layer),
|
||||
"contextual=%s hints=%d ids={%s} icon=dialog-question-symbolic(%s); tour=%s steps=%d "
|
||||
"step=%d auto=%d requested=%d",
|
||||
"contextual=%s hints=%d ids={%s} icon=dialog-information-symbolic(%s) hint=%dx%d; "
|
||||
"tour=%s steps=%d step=%d auto=%d requested=%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.tour_requested ? 1 : 0);
|
||||
icon_ok ? "ok" : "MISSING", hint_w, hint_h,
|
||||
app.tour.running ? "running" : "idle", tour_step_count(app), tour_step_index(app),
|
||||
app.tour_auto_started ? 1 : 0, app.tour_requested ? 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;
|
||||
app.dbg_tour_visible = app.tour.running ? 1 : 0;
|
||||
|
||||
std::string figures;
|
||||
for (int i = 0; i < kFigureSurfaceCount; ++i) {
|
||||
|
||||
Reference in new issue
Block a user