ui: the tour and the contextual help follow the badge bar, the slide-over and the dock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-02 20:46:59 +01:00
1 parent 4b65197f56
commit 2826042156
5 files changed
+254 -119

No files matched your search

+19 -3
View File
@@ -73,9 +73,11 @@ enum class EventGroupKind {
/// do not exist yet.
struct TourStop {
GtkWidget* target;
/// The named page of the view's `page_stack` the target lives on, or nullptr when the stop needs no
/// page. The tour selects it as the stop is entered, so a target on a page the user has left is
/// made visible instead of being walked over or spotlighted where it is not.
/// The page the target lives on, or nullptr when the stop needs none. What a page *is* depends on
/// the view: a named page of its `page_stack` (the chooser), or whatever its `on_page` hook opens
/// (the main window: a slide-over page id). The tour selects it as the stop is entered, so a target
/// on a page the user has left is made visible instead of being walked over or spotlighted where it
/// is not. A stop with no page *closes* what an `on_page` hook opened.
const char* page;
const char* title;
const char* body;
@@ -103,6 +105,16 @@ struct TourView {
/// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk
/// begins, when the widgets exist; null leaves every stop on the page it finds.
GtkWidget* page_stack = nullptr;
/// The alternative to `page_stack` for a surface that is not a stack: when set it *replaces* the
/// stack mechanism for this view, and is called with each stop's `page` as the stop is entered --
/// `nullptr` meaning "no page needed" -- and with `nullptr` as the walk ends. The main window's opens
/// and closes the slide-over through it; the chooser keeps `page_stack`.
void (*on_page)(void* user, const char* page) = nullptr;
void* page_user = nullptr;
/// Optional, with `on_page`: whether the surface `on_page` opened has finished appearing. A stop that
/// names a page is not spotlit while this answers false -- a panel that is still sliding in is a
/// target that moves, and a spotlight that follows it for 120 ms reads as a glitch. Null means "ready".
bool (*page_settled)(void* user) = nullptr;
/// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour
/// on to the application through `Config::tour_pending`.
void (*on_done)(void* user) = nullptr;
@@ -2114,6 +2126,10 @@ void tour_end(TourView& view);
void tour_update(TourView& view);
/// The widget the current stop points at, or nullptr when the walk is not on a stop.
GtkWidget* tour_target(TourView& view);
/// The part of `target` that is on screen, in `overlay`'s coordinates: its bounds intersected with every
/// scrolled window above it. False when it cannot be measured yet or is scrolled out of sight entirely.
/// The spotlight is drawn from it, and the probe reads the spotlight against it.
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out);
/// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted.
void build_tour(App& app);
+67 -45
View File
@@ -6657,8 +6657,8 @@ GtkWidget* helpui_menu_view(App* a) {
}
/// One line: the spotlight, the widget it spotlights, and the card, all in **window coordinates** --
/// the frame a screenshot is cropped in -- with the window's own record of the sidebar page beside
/// them, so a stop that had to change the page to find its target is readable as such. `contains` is
/// the frame a screenshot is cropped in -- with the slide-over's open page beside them (`none` when it
/// is closed), so a stop that had to open a page to find its target is readable as such. `contains` is
/// the spotlight covering its target (it is the target's own rectangle, grown three pixels) and
/// `clear` is the card missing the target. `probe` is the tag it is printed under, so a window with
/// more than one tour probe can tell its lines apart.
@@ -6670,7 +6670,7 @@ void tour_geometry_line(App& a, const char* probe, const char* what) {
const bool have_spot =
a.tour.spot != nullptr && gtk_widget_compute_bounds(a.tour.spot, a.window, &spot);
const bool have_target = t != nullptr && gtk_widget_get_mapped(t) &&
gtk_widget_compute_bounds(t, a.window, &target);
tour_target_bounds(t, a.window, &target);
const bool have_card =
a.tour.card != nullptr && gtk_widget_compute_bounds(a.tour.card, a.window, &card);
const auto contains = [](const graphene_rect_t& outer, const graphene_rect_t& inner) {
@@ -6859,6 +6859,10 @@ gboolean helpui_probe_step(gpointer user) {
// The trigger pane's summary grid is on screen only in *locked* mode, and one of the help's
// figures is that content: the probe reads `help figures:` in both states, so the geometry agent
// has the locked pane's numbers without having to guess how to get there.
// The trigger form is a slide-over page: it is open for both readings (and for the collapsed-search
// one after them), because a closed panel leaves every one of its surfaces unmapped and reads
// `-1,-1,0,0`. The 400 ms of the step is past the 120 ms reveal.
if (step == 6) slideover_open(*a, "trigger");
if (a->trigger_edit != nullptr) g_signal_emit_by_name(a->trigger_edit, "clicked");
helpui_probe_next(*a, step == 6 ? 7 : 9, 400);
return G_SOURCE_REMOVE;
@@ -6891,6 +6895,7 @@ gboolean helpui_probe_step(gpointer user) {
hint != nullptr && gtk_widget_get_mapped(hint) ? 1 : 0,
gtk_widget_get_mapped(a->trigger_search) ? 1 : 0, sw, sh);
}
slideover_close(*a); // opened by step 6
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
@@ -6898,7 +6903,7 @@ gboolean helpui_probe_step(gpointer user) {
// --- the tour's surface switch: a stop that stands on another stack page -------
/// Whether the walk's plan holds `target`. `steps=` alone cannot say a stop was *filtered out* rather
/// than merely reordered, so the two cases the filter tells apart are read against the plan itself.
/// than merely reordered, so the cases the filter tells apart are read against the plan itself.
bool tour_plan_holds(const App& a, GtkWidget* target) {
for (const TourStop& stop : a.tour.plan) {
if (stop.target == target) return true;
@@ -6912,27 +6917,34 @@ void tour_surface_probe_next(App& a, guint delay_ms) {
g_timeout_add(delay_ms, tour_surface_probe_step, &a);
}
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: the stop that stands on a page it is not on. It puts the sidebar
/// on **Measure**, starts the window's tour, and walks it a stop per tick -- a tick apart on purpose,
/// because a stack's new page is allocated on the frame *after* the switch, so a reading taken in the
/// switch's own tick would be of the page the walk has just left (and `update_tour()` places the hole
/// from the frame that *has* been laid out). Every step prints the sidebar's page and whether the
/// stop's target is mapped, and the stop whose target is the signal list prints the same geometry
/// `[helpui]` does: **`side_tab=signals` with `target=signal_list` and `contains=1`** is the reading
/// that the stop was not merely kept but *made* visible. It then puts the sidebar away --
/// `sidebar_wanted` is the wish the tick re-asserts -- and starts the tour again: a *collapsed* pane
/// leaves the stack unmapped, so the stops that need it are dropped and `steps=` falls. That pair is
/// the filter's two cases, read rather than assumed.
/// The slide-over's open page for a reading: its id, or `none`.
const char* slideover_name(const App& a) {
return slideover_page(a).empty() ? "none" : slideover_page(a).c_str();
}
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: the stop that opens a surface it is not on. It opens the
/// slide-over's `trigger` page, starts the window's tour from that state, and walks the whole table a
/// stop per tick -- a tick apart on purpose (400 ms, longer than the panel's 120 ms slide), because a
/// reading taken in the switch's own tick would be of the surface the walk has just left (and
/// `update_tour()` places the hole from the frame that *has* been laid out). Every stop prints its
/// title, the slide-over's open page and whether the target is mapped, with the spotlight geometry
/// `[helpui]` prints: **`slideover=trigger` at the trigger stop and `slideover=none` at the one after
/// it, with `contains=1`**, is the reading that the walk opened the panel and then closed it. The walk
/// then runs again and is dismissed on the trigger stop (the Skip/Esc route), which must close the
/// panel too. It finally puts the dock *away* -- `sidebar_wanted` is the wish the tick re-asserts --
/// and starts the tour once more: the dock's stops are dropped (a collapsed pane is not walked over)
/// while the trigger stop, which does not depend on the dock, is kept.
gboolean tour_surface_probe_step(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
const auto mapped = [](GtkWidget* w) { return w != nullptr && gtk_widget_get_mapped(w) ? 1 : 0; };
switch (a->dbg_tour_surface_step) {
case 0: {
// Where the run starts: a fresh config's sidebar is on Signals, so this is a *first* run's
// state. The auto-start's walk (if one began) would only confuse the reading; it goes first.
// Where the run starts: the panel closed and the dock on screen. The auto-start's walk (if one
// began) would only confuse the reading; it goes first. The panel is then opened, so the walk's
// first stop (which needs no page) has something to close.
std::fprintf(stderr, "[toursurface] before: slideover=%s signal_list_mapped=%d running=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
slideover_name(*a), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
tour_dismiss(*a);
slideover_open(*a, "trigger");
a->dbg_tour_surface_step = 1;
@@ -6940,15 +6952,16 @@ gboolean tour_surface_probe_step(gpointer user) {
return G_SOURCE_REMOVE;
}
case 1: {
// A frame after the switch, so the Measure page has been laid out and the list really is off
// screen. The tour is started *here*, from this state.
// A frame after the page opened, so the panel has slid in. The tour is started *here*, from this
// state.
std::fprintf(stderr,
"[toursurface] on trigger: slideover=%s signal_list_mapped=%d side_box_mapped=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
mapped(a->signal_list), mapped(a->side_box));
"[toursurface] on trigger: slideover=%s trigger_body_mapped=%d side_box_mapped=%d\n",
slideover_name(*a), mapped(a->trigger_body), mapped(a->side_box));
start_tour(*a);
std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n",
std::fprintf(stderr,
"[toursurface] tour: visible=%d steps=%d step=%d trigger_stop=%d signals_stop=%d\n",
tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a),
tour_plan_holds(*a, a->trigger_body) ? 1 : 0,
tour_plan_holds(*a, a->signal_list) ? 1 : 0);
a->dbg_tour_surface_step = 2;
tour_surface_probe_next(*a, 400);
@@ -6956,44 +6969,53 @@ gboolean tour_surface_probe_step(gpointer user) {
}
case 2: {
if (!tour_active(*a)) {
std::fprintf(stderr, "[toursurface] walk ended before the signals stop\n");
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
GtkWidget* target = tour_target(*a);
const bool is_list = target != nullptr && target == a->signal_list;
std::fprintf(stderr, "[toursurface] step=%d/%d slideover=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a),
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(target),
is_list ? " target=signal_list" : "");
if (is_list) {
// The frame after the switch: the page has been laid out and `update_tour()` has placed the
// hole from it, so this is the reading that the spotlight is on the list.
tour_geometry_line(*a, "toursurface", "signals stop");
tour_dismiss(*a);
// The last Next ended the walk: the panel must have gone with it.
std::fprintf(stderr, "[toursurface] finished: running=0 slideover=%s\n", slideover_name(*a));
start_tour(*a);
while (tour_active(*a) && tour_target(*a) != a->trigger_body) tour_step(*a, 1);
a->dbg_tour_surface_step = 3;
tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE;
}
GtkWidget* target = tour_target(*a);
const TourStop& stop = a->tour.plan[static_cast<std::size_t>(tour_step_index(*a))];
std::fprintf(stderr, "[toursurface] step=%d/%d title='%s' slideover=%s target_mapped=%d\n",
tour_step_index(*a) + 1, tour_step_count(*a), stop.title, slideover_name(*a),
mapped(target));
// The frame after the switch: the page has been laid out and `update_tour()` has placed the
// hole from it, so this is the reading that the spotlight is on the stop's target.
tour_geometry_line(*a, "toursurface", "stop");
tour_step(*a, 1);
tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE;
}
case 3: {
// The other half of the filter. `sidebar_wanted` is the user's wish that the tick re-asserts
// (`update_side_visibility`), so setting it here is how a probe puts the sidebar away for good;
// the delay leaves the split view time to animate the column shut and unmap it.
a->sidebar_wanted = false;
// The walk again, parked on the trigger stop with the panel open: Skip (or Esc) closes it.
std::fprintf(stderr, "[toursurface] restarted at trigger: running=%d slideover=%s\n",
tour_active(*a) ? 1 : 0, slideover_name(*a));
tour_dismiss(*a);
std::fprintf(stderr, "[toursurface] skipped: running=%d slideover=%s\n",
tour_active(*a) ? 1 : 0, slideover_name(*a));
a->dbg_tour_surface_step = 4;
tour_surface_probe_next(*a, 100);
return G_SOURCE_REMOVE;
}
case 4: {
// The other half of the filter. `sidebar_wanted` is the user's wish that the tick re-asserts
// (`update_side_visibility`), so setting it here is how a probe puts the dock away for good; the
// delay leaves the split view time to animate the column shut and unmap it.
a->sidebar_wanted = false;
a->dbg_tour_surface_step = 5;
tour_surface_probe_next(*a, 700);
return G_SOURCE_REMOVE;
}
default: {
start_tour(*a);
std::fprintf(stderr,
"[toursurface] collapsed: side_box_mapped=%d steps=%d signals_stop=%d "
"side_stop=%d\n",
"[toursurface] collapsed: side_box_mapped=%d steps=%d trigger_stop=%d "
"signals_stop=%d side_stop=%d\n",
mapped(a->side_box), tour_step_count(*a),
tour_plan_holds(*a, a->trigger_body) ? 1 : 0,
tour_plan_holds(*a, a->signal_list) ? 1 : 0,
tour_plan_holds(*a, a->side_box) ? 1 : 0);
tour_dismiss(*a);
+101 -28
View File
@@ -16,9 +16,9 @@
// (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
// There are *two* tours and one implementation. The main window's walks the plot, the badge bar, the
// trigger (in the slide-over), the dock 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.
@@ -76,12 +76,18 @@ const TourStop* current_stop(const TourView& view) {
return &view.plan[static_cast<std::size_t>(view.index)];
}
/// Selects a stop's page on the view's stack, before anything is measured against the target. Both
/// windows' stacks are `AdwViewStack`s and their page names are the ones the same tables name their
/// `.ui` pages with, so the machinery can do this itself; the `get_child_by_name()` guard keeps a
/// stop that names a page a later `.ui` dropped from blanking the stack. The stack's own
/// `notify::visible-child-name` is what tells the window which page it is now on.
/// Selects a stop's page before anything is measured against the target. Two mechanisms, one per
/// kind of view: a view with an `on_page` hook (the main window: its pages are slide-over pages, and
/// a stop with no page *closes* the panel) hands the page to the hook; a view with a `page_stack` (the
/// source chooser: an `AdwViewStack` whose page names are the ones its `.ui` names its pages with)
/// has the machinery select it. The `get_child_by_name()` guard keeps a stop that names a page a
/// later `.ui` dropped from blanking the stack. The stack's own `notify::visible-child-name` is what
/// tells the window which page it is now on.
void select_view_page(TourView& view, const char* page) {
if (view.on_page != nullptr) {
view.on_page(view.page_user, page);
return;
}
if (view.page_stack == nullptr || page == nullptr) return;
if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(view.page_stack), page) == nullptr) return;
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(view.page_stack), page);
@@ -91,13 +97,16 @@ void select_view_page(TourView& view, const char* page) {
/// pages, and the split view its sidebar, with `gtk_widget_set_child_visible()`, which leaves the
/// widget's own visible flag -- the one `is_visible()` walks up the parents -- **true**. So the only
/// honest "is this on screen" test is whether it is mapped, and it is the one that keeps the filter's
/// two cases apart:
/// cases apart:
/// * a stop that names a page of a view with an `on_page` hook is always walkable: the hook opens
/// the page whatever the rest of the window is doing (the slide-over does not depend on the dock);
/// * a target not mapped because it is on *another page* is walked over when the stop names that
/// page and the stack itself is on screen -- selecting the page is what puts the target there;
/// * a target not mapped because its *pane is collapsed* leaves the stack unmapped too, so no page
/// can reveal it and the stop is still dropped.
bool stop_is_walkable(const TourView& view, const TourStop& stop) {
if (stop.target == nullptr) return false;
if (stop.page != nullptr && view.on_page != nullptr) return true;
if (gtk_widget_get_mapped(stop.target)) return true;
return stop.page != nullptr && view.page_stack != nullptr &&
gtk_widget_get_mapped(view.page_stack);
@@ -130,6 +139,25 @@ void show_step(TourView& view) {
} // namespace
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out) {
if (target == nullptr || overlay == nullptr || out == nullptr) return false;
if (!gtk_widget_compute_bounds(target, overlay, out)) return false;
// A target taller than the scroller it stands in (a form on a short window) is clipped by that
// scroller: the part below its edge is not on screen, and a hole that kept going would spotlight
// whatever is under the scroller instead -- the badge bar, the status line. So the hole is the
// *visible* part, intersected with every scrolled window above the target.
for (GtkWidget* parent = gtk_widget_get_parent(target); parent != nullptr;
parent = gtk_widget_get_parent(parent)) {
if (!GTK_IS_SCROLLED_WINDOW(parent)) continue;
graphene_rect_t clip;
graphene_rect_t cut;
if (!gtk_widget_compute_bounds(parent, overlay, &clip)) continue;
if (!graphene_rect_intersection(out, &clip, &cut)) return false;
*out = cut;
}
return true;
}
// --- the machinery, shared by both windows ----------------------------------
void tour_build(TourView& view, GtkWidget* overlay) {
@@ -241,6 +269,10 @@ void tour_advance(TourView& view, int delta) {
}
void tour_end(TourView& view) {
// A page the walk opened is the walk's to close: the panel must not be left standing over a window
// whose tour has ended (Skip, Esc, or the last Next). Only when a walk was actually running --
// dismissing an idle tour must not close a panel the user opened.
if (view.running && view.on_page != nullptr) view.on_page(view.page_user, nullptr);
view.running = false;
view.index = 0;
view.plan.clear();
@@ -271,8 +303,15 @@ void tour_update(TourView& view) {
// Mapped, not `is_visible()`: a widget on another page of a stack, or in a collapsed pane, keeps
// its own visible flag -- a stack hides its pages with `set_child_visible()` -- and a widget that
// is not on screen is not one to leave the spotlight standing on.
const bool target_present = target != nullptr && gtk_widget_get_mapped(target);
if (target_present && gtk_widget_compute_bounds(target, view.overlay, &bounds)) {
bool target_present = target != nullptr && gtk_widget_get_mapped(target);
// A stop on a page waits for the page to finish appearing: until then the target is moving, and
// the frame is drawn as a stop with nothing to point at (everything dim, the hole empty).
const TourStop* stop = current_stop(view);
if (target_present && stop != nullptr && stop->page != nullptr && view.page_settled != nullptr &&
!view.page_settled(view.page_user)) {
target_present = false;
}
if (target_present && tour_target_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.
@@ -354,6 +393,13 @@ void tour_update(TourView& view) {
} else if (hx >= cw + 2 * kGap) {
cx = hx - cw - kGap;
cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap));
} else if (hx > w - (hx + hw) + kGap) {
// No side wide enough for the card, but the hole is at the right edge (the slide-over's trigger
// form on a narrow window: 276 px to its left against a 289 px card): the card goes flush left,
// overlapping the plot and as little of the target as there is no way to avoid, instead of
// sitting on the target's own corner.
cx = 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
@@ -374,9 +420,9 @@ namespace {
/// point of re-measuring every frame is that nothing here knows where the widget is.
struct TourStep {
const char* id;
/// The sidebar page the target lives on, or nullptr when the stop is not on either page. The
/// trigger and the channel list are both on the Signals page, so a stop about either has to put
/// the sidebar there before it can spotlight one.
/// The slide-over page the target lives on, or nullptr when the stop needs none. A stop with a page
/// opens it; a stop without one closes the panel, so the card never points at the status line over
/// an open panel. The page ids are the slide-over's (`trigger`, `source-<id>`, ...).
const char* page;
const char* title;
const char* body;
@@ -388,15 +434,20 @@ constexpr TourStep kTourSteps[] = {
"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", nullptr, "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", "signals", "The trigger",
{"badges", nullptr, "The badges",
"Setup is reached by the thing it configures. The Trigger badge opens the trigger form; the "
"Source badge opens the source's own controls. Click it again, press Esc or click the plot to "
"close the panel.",
&App::badge_bar},
{"trigger", "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", "signals", "The signals",
{"side", nullptr, "The dock",
"The dock beside the plot: the list of signals, then the cursors and the measurements. The "
"buttons in the bar above show and hide it.",
&App::side_box},
{"signals", nullptr, "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},
@@ -407,6 +458,23 @@ constexpr TourStep kTourSteps[] = {
};
constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0]));
/// The tour's page hook: a page is a slide-over page, and no page closes the panel.
void tour_open_page(void* user, const char* page) {
App& app = *static_cast<App*>(user);
if (page != nullptr) {
slideover_open(app, page);
} else {
slideover_close(app);
}
}
/// Whether the slide-over has finished sliding in. A closed panel has nothing to wait for.
bool tour_page_settled(void* user) {
const App& app = *static_cast<App*>(user);
if (app.slideover.revealer == nullptr || app.slideover.page.empty()) return true;
return gtk_revealer_get_child_revealed(GTK_REVEALER(app.slideover.revealer)) != FALSE;
}
} // namespace
void build_tour(App& app) {
@@ -422,9 +490,12 @@ void start_tour(App& app) {
app.settings.tour_seen = true;
(void)save_settings(app.settings);
}
// The main window has no page stack any more (the dock is one scroller): `page_stack` stays null, so
// a stop's `page` selects nothing and a stop whose target is unmapped is dropped. The slide-over hook
// that lets a stop open the trigger page is the next step's.
// The main window has no page stack (the dock is one scroller): its pages are the slide-over's, and
// the view's `on_page` hook opens one (or closes the panel for a stop with no page), replacing the
// `page_stack` mechanism the chooser keeps.
app.tour.on_page = tour_open_page;
app.tour.page_settled = tour_page_settled;
app.tour.page_user = &app;
// The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen.
std::vector<TourStop> stops;
stops.reserve(kTourStepCount);
@@ -484,7 +555,8 @@ constexpr FigureSurface kFigureSurfaces[] = {
{"trigger_summary", &App::trigger_summary},
{"trigger_panel", &App::trigger_panel},
{"signal_list", &App::signal_list},
{"side_stack", &App::side_box},
{"badge_bar", &App::badge_bar},
{"dock", &App::side_box},
{"window_button", &App::window_button},
{"status_label", &App::status_label},
{"header_bar", &App::toolbar},
@@ -526,10 +598,11 @@ void capture_help_state(App& app) {
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) &&
// `is_visible()` and `get_mapped()`, not `get_visible()`: a widget under a hidden parent -- the
// trigger summary while the pane is in configuration mode -- keeps its own flag true and reads
// 0x0, and so does everything on the slide-over while the panel is closed (a revealer unmaps its
// child without touching its visible flag); a crop box would take either for a real rectangle.
if (widget != nullptr && gtk_widget_is_visible(widget) && gtk_widget_get_mapped(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));