fix: the guided tour selects the page a stop's target lives on

A stop whose target is on another page of a stack was entered without
switching to that page, so the spotlight aimed at a widget that was not
on screen. A stop now declares the page it stands on (`TourStop::page`)
and the tour selects it as the step is entered -- the window's trigger
and signals stops put the sidebar on Signals, the chooser's tabs stop
shows the Waveforms page and stepping off it brings Sources back.

The stop filter moved into `tour_begin()` and now runs on
`gtk_widget_get_mapped()`: GTK 4.24's `is_visible()` ignores
`child_visible`, so it kept a stop on another page *and* one in a
collapsed pane alike. A wrong page is kept and selected; a collapsed
pane, whose stack is unmapped too, is still dropped.
This commit is contained in:
Martino Ferrari committed 2026-10-14 23:01:43 +02:00
1 parent 829176248a
commit d549223889
9 files changed
+385 -89

No files matched your search

+120 -9
View File
@@ -4557,16 +4557,19 @@ 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. `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.
void helpui_report_geometry(App& a, const char* what) {
/// 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 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.
void tour_geometry_line(App& a, const char* probe, const char* what) {
graphene_rect_t spot = GRAPHENE_RECT_INIT(0, 0, 0, 0);
graphene_rect_t target = GRAPHENE_RECT_INIT(0, 0, 0, 0);
graphene_rect_t card = GRAPHENE_RECT_INIT(0, 0, 0, 0);
GtkWidget* t = tour_target(a);
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_is_visible(t) &&
const bool have_target = t != nullptr && gtk_widget_get_mapped(t) &&
gtk_widget_compute_bounds(t, a.window, &target);
const bool have_card =
a.tour.card != nullptr && gtk_widget_compute_bounds(a.tour.card, a.window, &card);
@@ -4580,11 +4583,11 @@ void helpui_report_geometry(App& a, const char* what) {
p.origin.y < q.origin.y + q.size.height && q.origin.y < p.origin.y + p.size.height;
};
std::fprintf(stderr,
"[helpui] %s=%d/%d visible=%d spot=[%.0f,%.0f %.0fx%.0f] "
"[%s] %s=%d/%d visible=%d side_tab=%s spot=[%.0f,%.0f %.0fx%.0f] "
"target=[%.0f,%.0f %.0fx%.0f] contains=%d card=[%.0f,%.0f %.0fx%.0f] clear=%d\n",
what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0,
spot.origin.x, spot.origin.y, spot.size.width, spot.size.height, target.origin.x,
target.origin.y, target.size.width, target.size.height,
probe, what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0,
a.side_tab.c_str(), spot.origin.x, spot.origin.y, spot.size.width, spot.size.height,
target.origin.x, target.origin.y, target.size.width, target.size.height,
have_spot && have_target && contains(spot, target) ? 1 : 0, card.origin.x,
card.origin.y, card.size.width, card.size.height,
have_card && have_target && !overlaps(card, target) ? 1 : 0);
@@ -4715,7 +4718,7 @@ gboolean helpui_probe_step(gpointer user) {
// spotlight must *contain* the target, and the card must not overlap it when the window has room
// for a card beside or above/below it. All of it is an allocation, so it is read one frame after
// the step it describes: `update_tour()` places this frame from the allocation the last one left.
helpui_report_geometry(*a, "step");
tour_geometry_line(*a, "helpui", "step");
if (step == 4) {
// Next, which is what a user presses to walk the tour.
tour_step(*a, 1);
@@ -4774,5 +4777,113 @@ gboolean helpui_probe_step(gpointer user) {
return G_SOURCE_REMOVE;
}
// --- 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.
bool tour_plan_holds(const App& a, GtkWidget* target) {
for (const TourStop& stop : a.tour.plan) {
if (stop.target == target) return true;
}
return false;
}
gboolean tour_surface_probe_step(gpointer user);
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.
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.
std::fprintf(stderr, "[toursurface] before: side_tab=%s signal_list_mapped=%d running=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
tour_dismiss(*a);
select_side_tab(*a, "measure");
a->dbg_tour_surface_step = 1;
tour_surface_probe_next(*a, 500);
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.
std::fprintf(stderr,
"[toursurface] on measure: side_tab=%s signal_list_mapped=%d side_stack_mapped=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), mapped(a->side_stack));
start_tour(*a);
std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n",
tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a),
tour_plan_holds(*a, a->signal_list) ? 1 : 0);
a->dbg_tour_surface_step = 2;
tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE;
}
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 side_tab=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a), a->side_tab.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);
a->dbg_tour_surface_step = 3;
tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE;
}
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;
a->dbg_tour_surface_step = 4;
tour_surface_probe_next(*a, 700);
return G_SOURCE_REMOVE;
}
default: {
start_tour(*a);
std::fprintf(stderr,
"[toursurface] collapsed: side_stack_mapped=%d steps=%d signals_stop=%d "
"side_stop=%d\n",
mapped(a->side_stack), tour_step_count(*a),
tour_plan_holds(*a, a->signal_list) ? 1 : 0,
tour_plan_holds(*a, a->side_box) ? 1 : 0);
tour_dismiss(*a);
a->sidebar_wanted = true;
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
}
return G_SOURCE_REMOVE;
}
} // namespace ui_impl
} // namespace udpscope