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:
1 parent
829176248a
commit
d549223889
9 files changed
+385
-89
No files matched your search
+63
-15
@@ -76,10 +76,42 @@ 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.
|
||||
void select_view_page(TourView& view, const char* page) {
|
||||
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);
|
||||
}
|
||||
|
||||
/// Whether a stop belongs in the walk. `get_mapped()`, not `is_visible()`: a stack hides its other
|
||||
/// 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:
|
||||
/// * 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 (gtk_widget_get_mapped(stop.target)) return true;
|
||||
return stop.page != nullptr && view.page_stack != nullptr &&
|
||||
gtk_widget_get_mapped(view.page_stack);
|
||||
}
|
||||
|
||||
/// 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 surface first, and nothing is measured here: the stop may live on a page the user has left,
|
||||
// and a stack's new page is allocated on the frame *after* the switch, so a one-shot measure in
|
||||
// this tick would read the page the walk has just left. `tour_update()` runs every frame and fills
|
||||
// the hole in on the next one.
|
||||
select_view_page(view, stop->page);
|
||||
// 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.
|
||||
view.hole[0] = view.hole[1] = view.hole[2] = view.hole[3] = 0;
|
||||
@@ -170,8 +202,16 @@ void tour_build(TourView& view, GtkWidget* overlay) {
|
||||
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;
|
||||
// The walk is the stops whose surface is on screen *or* can be selected into view, filtered here
|
||||
// rather than by each caller so the two windows cannot drift into two rules -- and because the
|
||||
// rule needs the view's own `page_stack`, which the caller's table does not carry. See
|
||||
// `stop_is_walkable()` for what separates a wrong page from a collapsed pane.
|
||||
std::vector<TourStop> plan;
|
||||
for (const TourStop& stop : stops) {
|
||||
if (stop_is_walkable(view, stop)) plan.push_back(stop);
|
||||
}
|
||||
if (plan.empty()) return;
|
||||
view.plan = std::move(plan);
|
||||
view.on_done = on_done;
|
||||
view.done_user = done_user;
|
||||
view.index = 0;
|
||||
@@ -228,7 +268,10 @@ void tour_update(TourView& view) {
|
||||
// one-shot measurement would leave the hole in the corner.
|
||||
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);
|
||||
// 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)) {
|
||||
// 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
|
||||
@@ -331,29 +374,33 @@ 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.
|
||||
const char* page;
|
||||
const char* title;
|
||||
const char* body;
|
||||
GtkWidget* App::* target;
|
||||
};
|
||||
|
||||
constexpr TourStep kTourSteps[] = {
|
||||
{"plot", "The plot",
|
||||
{"plot", nullptr, "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",
|
||||
{"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", "The trigger",
|
||||
{"trigger", "signals", "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",
|
||||
{"signals", "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",
|
||||
{"status", nullptr, "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},
|
||||
@@ -375,17 +422,18 @@ void start_tour(App& app) {
|
||||
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.
|
||||
// The sidebar's own stack, so a stop can name the page it stands on. Set here and not in
|
||||
// `build_tour()`: the overlay is inserted above the window's content *before* `App::side_stack` is
|
||||
// assigned, so the member does not exist yet at build time -- and the walk cannot start before the
|
||||
// window is up anyway.
|
||||
app.tour.page_stack = app.side_stack;
|
||||
// The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen.
|
||||
std::vector<TourStop> stops;
|
||||
stops.reserve(kTourStepCount);
|
||||
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});
|
||||
}
|
||||
stops.push_back({target, kTourSteps[i].page, kTourSteps[i].title, kTourSteps[i].body});
|
||||
}
|
||||
if (stops.empty()) return;
|
||||
tour_begin(app.tour, std::move(stops), nullptr, nullptr);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user