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:
1 parent
4b65197f56
commit
2826042156
5 files changed
+254
-119
No files matched your search
+19
-3
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user