ui: tighten the badge/dock/tour probes after review and fix the tour's stale hole
The session old-file probe and save_scheduled now discriminate, Esc is read through the real key path, the dock-section probe proves the Measure section starts out of view, and the tour's spotlight is measured independently of the helper it is drawn from. A target scrolled out of its scroller no longer leaves the previous hole standing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2826042156
commit
a1672acc70
7 files changed
+210
-62
No files matched your search
+96
-18
@@ -1825,6 +1825,7 @@ gboolean dock_section_probe_report(gpointer user) {
|
||||
}
|
||||
}
|
||||
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
|
||||
if (a->signal_panel != nullptr) gtk_widget_set_size_request(a->signal_panel, -1, -1);
|
||||
std::fprintf(stderr,
|
||||
"[side-tab-probe] section=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
|
||||
" min_n=%" G_GUINT64_FORMAT "\n",
|
||||
@@ -1846,6 +1847,10 @@ gboolean dock_section_probe(gpointer user) {
|
||||
if (!padded && a->dock_page != nullptr) {
|
||||
padded = true;
|
||||
gtk_widget_set_size_request(a->dock_page, -1, 3000);
|
||||
// ... and the signal list taller than any window too, which is what pushes the Measure section below
|
||||
// the viewport at offset 0 (padding the page's end only makes it scrollable, and on a window taller than
|
||||
// the section's y the section would stay in view). Undone in the report.
|
||||
if (a->signal_panel != nullptr) gtk_widget_set_size_request(a->signal_panel, -1, 3000);
|
||||
g_timeout_add(400, dock_section_probe, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
@@ -1886,10 +1891,19 @@ gboolean dock_section_probe(gpointer user) {
|
||||
if (a->measures_stats_check != nullptr) {
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
|
||||
}
|
||||
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
|
||||
// From the top, is the measure section (the cursors panel, where `dock_show_section("measure")` lands)
|
||||
// below the viewport? Only then is scrolling to it a move *into* view and back out a move *away*; a
|
||||
// section already in view at offset 0 would make the whole probe a reading of nothing.
|
||||
gtk_adjustment_set_value(dock_adj, 0.0);
|
||||
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
|
||||
graphene_point_t measure_at = GRAPHENE_POINT_INIT(-1.f, -1.f);
|
||||
const bool located =
|
||||
a->cursors_panel != nullptr && gtk_widget_compute_point(a->cursors_panel, a->dock_page, &zero, &measure_at);
|
||||
const bool outside_at_top = located && static_cast<double>(measure_at.y) >= gtk_adjustment_get_page_size(dock_adj);
|
||||
dock_show_section(*a, "measure");
|
||||
// Where the dock went: `scrolled` is its offset and `reach` the most it can scroll. A reach of 0 would
|
||||
// mean the content fits and "away from the section" is not a state this run can reach.
|
||||
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
|
||||
const double scrolled = gtk_adjustment_get_value(dock_adj);
|
||||
const double reach = gtk_adjustment_get_upper(dock_adj) - gtk_adjustment_get_page_size(dock_adj);
|
||||
update_measures(*a, /*force=*/true, /*accumulate=*/true);
|
||||
@@ -1898,8 +1912,11 @@ gboolean dock_section_probe(gpointer user) {
|
||||
// Away from the section the measurements are in: they are scrolled out of the viewport (the widgets
|
||||
// stay mapped, only clipped), and the tick keeps calling `update_measures()` regardless.
|
||||
dock_show_section(*a, "signals");
|
||||
std::fprintf(stderr, "[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f)\n",
|
||||
dock_section_in_view(*a), g_dock_section_n_before.size(), scrolled, reach);
|
||||
std::fprintf(stderr,
|
||||
"[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f, "
|
||||
"outside_at_top=%d)\n",
|
||||
dock_section_in_view(*a), g_dock_section_n_before.size(), scrolled, reach,
|
||||
outside_at_top ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
|
||||
g_timeout_add(2000, dock_section_probe_report, a);
|
||||
@@ -3607,13 +3624,23 @@ gboolean sidebar_probe(gpointer user) {
|
||||
/// Leg 1 hides the dock through the button (a wish, so it must schedule a save), collects the session and
|
||||
/// writes it, reads the file back and applies it; leg 2 does the same shown. Leg 3 loads a file written by
|
||||
/// the previous version (`side_tab = "measure"`, no `dock`): it must load without a complaint and leave
|
||||
/// `dock_shown` at its default, true. Read as `ok=1` on every line.
|
||||
/// `dock_shown` at its default, true (the probe presets it to false, so only a load that resets it passes).
|
||||
/// Each leg first cancels any pending save timer and expects `save_scheduled=1` only with a source port.
|
||||
/// Read as `ok=1` on every line.
|
||||
gboolean dock_session_probe(gpointer user) {
|
||||
App& a = *static_cast<App*>(user);
|
||||
if (a.window == nullptr || a.sidebar_toggle == nullptr) return G_SOURCE_REMOVE;
|
||||
const std::string dir = g_get_tmp_dir();
|
||||
const std::string path = dir + "/udpscope-dock-session-probe.toml";
|
||||
// `schedule_layout_save()` is an idempotent 400 ms debounce and does nothing without a source port
|
||||
// (a recording opened from a file has none), so a leg that did not cancel the previous leg's timer
|
||||
// would read it as its own. Cancel first, and expect a save only where one can be scheduled at all.
|
||||
const bool can_save = a.source_port != 0;
|
||||
const auto leg = [&](const char* name, bool shown) {
|
||||
if (a.layout_save_source != 0) {
|
||||
g_source_remove(a.layout_save_source);
|
||||
a.layout_save_source = 0;
|
||||
}
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.sidebar_toggle), shown);
|
||||
const bool wish = a.sidebar_wanted;
|
||||
const bool save_pending = a.layout_save_source != 0;
|
||||
@@ -3633,8 +3660,8 @@ gboolean dock_session_probe(gpointer user) {
|
||||
"applied=%d split_follows=%d ok=%d\n",
|
||||
name, wish ? 1 : 0, save_pending ? 1 : 0, collected ? 1 : 0, saved ? 1 : 0,
|
||||
loaded ? 1 : 0, back.dock_shown ? 1 : 0, applied ? 1 : 0, split_follows ? 1 : 0,
|
||||
(wish == shown && save_pending && collected == shown && saved && loaded &&
|
||||
back.dock_shown == shown && applied)
|
||||
(wish == shown && collected == shown && saved && loaded &&
|
||||
back.dock_shown == shown && applied && save_pending == can_save)
|
||||
? 1 : 0);
|
||||
};
|
||||
leg("hidden", false);
|
||||
@@ -3648,10 +3675,10 @@ gboolean dock_session_probe(gpointer user) {
|
||||
LayoutState old_file;
|
||||
old_file.dock_shown = false; // anything but the default, so "left at the default" is a reading
|
||||
const bool loaded = load_layout(path, old_file);
|
||||
LayoutState fresh;
|
||||
const LayoutState fresh;
|
||||
std::fprintf(stderr, "[dock-session] old file (side_tab only): loaded=%d dock_shown=%d default=%d ok=%d\n",
|
||||
loaded ? 1 : 0, old_file.dock_shown ? 1 : 0, fresh.dock_shown ? 1 : 0,
|
||||
loaded && fresh.dock_shown ? 1 : 0);
|
||||
loaded && old_file.dock_shown == fresh.dock_shown ? 1 : 0);
|
||||
}
|
||||
std::remove(path.c_str());
|
||||
std::fflush(stderr);
|
||||
@@ -6411,6 +6438,19 @@ gboolean keys_probe(gpointer user) {
|
||||
before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor);
|
||||
before_after("s: side panel", a->sidebar_toggle, AppAction::SidePanel);
|
||||
|
||||
// Escape through the real key path: with a slide-over page open it must be consumed and close the page
|
||||
// (the handler runs the slide-over ahead of the editable check), and with none open it must be left alone.
|
||||
{
|
||||
const bool opened = slideover_open(*a, "trigger") && slideover_page(*a) == "trigger";
|
||||
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_Escape, 0, static_cast<GdkModifierType>(0), a);
|
||||
const bool closed = slideover_page(*a).empty();
|
||||
const gboolean idle = on_app_key_pressed(nullptr, GDK_KEY_Escape, 0, static_cast<GdkModifierType>(0), a);
|
||||
char buf[96];
|
||||
std::snprintf(buf, sizeof(buf), " esc: opened=%d handled=%d closed=%d idle_handled=%d |", opened ? 1 : 0,
|
||||
handled != FALSE ? 1 : 0, closed ? 1 : 0, idle != FALSE ? 1 : 0);
|
||||
line += buf;
|
||||
}
|
||||
|
||||
// An accel must be refused while text is being typed. Focus the window entry, send 'm'
|
||||
// through the real handler, and require the dock not to scroll.
|
||||
int refused = -1;
|
||||
@@ -6658,19 +6698,46 @@ 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 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
|
||||
/// is closed), so a stop that had to open a page to find its target is readable as such.
|
||||
///
|
||||
/// `raw` is the target's own rectangle, unclipped, and `target` the part of it a scrolled window above
|
||||
/// it leaves on screen -- measured *here*, by intersecting `raw` with each scroller's bounds, and not by
|
||||
/// `tour_target_bounds()` (which the spotlight is drawn from, so reading the spotlight against it would
|
||||
/// only agree with itself). `contains` is the spotlight covering that visible part (the part, grown three
|
||||
/// pixels), or being empty when none of the target is visible; `inside` is the spotlight staying within
|
||||
/// the nearest scroller above the target (grown by the same three pixels), `-1` when there is none;
|
||||
/// `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 raw = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||
graphene_rect_t target = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||
graphene_rect_t scroller = 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_get_mapped(t) &&
|
||||
tour_target_bounds(t, a.window, &target);
|
||||
const bool have_raw = t != nullptr && gtk_widget_get_mapped(t) &&
|
||||
gtk_widget_compute_bounds(t, a.window, &raw);
|
||||
target = raw;
|
||||
bool visible_part = have_raw;
|
||||
bool have_scroller = false;
|
||||
for (GtkWidget* p = have_raw ? gtk_widget_get_parent(t) : nullptr; p != nullptr;
|
||||
p = gtk_widget_get_parent(p)) {
|
||||
graphene_rect_t clip;
|
||||
if (!GTK_IS_SCROLLED_WINDOW(p) || !gtk_widget_compute_bounds(p, a.window, &clip)) continue;
|
||||
if (!have_scroller) {
|
||||
scroller = clip;
|
||||
have_scroller = true;
|
||||
}
|
||||
graphene_rect_t cut;
|
||||
if (!graphene_rect_intersection(&target, &clip, &cut)) {
|
||||
visible_part = false;
|
||||
target = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||
break;
|
||||
}
|
||||
target = cut;
|
||||
}
|
||||
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) {
|
||||
@@ -6682,15 +6749,26 @@ void tour_geometry_line(App& a, const char* probe, const char* what) {
|
||||
return p.origin.x < q.origin.x + q.size.width && q.origin.x < p.origin.x + p.size.width &&
|
||||
p.origin.y < q.origin.y + q.size.height && q.origin.y < p.origin.y + p.size.height;
|
||||
};
|
||||
// Nothing of the target on screen: the right spotlight is no spotlight at all.
|
||||
const bool spot_empty = have_spot && spot.size.width <= 0.5f && spot.size.height <= 0.5f;
|
||||
const bool covers = have_spot && have_raw && (visible_part ? contains(spot, target) : spot_empty);
|
||||
int inside = -1;
|
||||
if (have_scroller && have_spot) {
|
||||
graphene_rect_t grown = GRAPHENE_RECT_INIT(scroller.origin.x - 3.0f, scroller.origin.y - 3.0f,
|
||||
scroller.size.width + 6.0f, scroller.size.height + 6.0f);
|
||||
inside = (spot_empty || contains(grown, spot)) ? 1 : 0;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[%s] %s=%d/%d visible=%d slideover=%s spot=[%.0f,%.0f %.0fx%.0f] "
|
||||
"target=[%.0f,%.0f %.0fx%.0f] contains=%d card=[%.0f,%.0f %.0fx%.0f] clear=%d\n",
|
||||
"raw=[%.0f,%.0f %.0fx%.0f] target=[%.0f,%.0f %.0fx%.0f] contains=%d inside=%d "
|
||||
"card=[%.0f,%.0f %.0fx%.0f] clear=%d\n",
|
||||
probe, what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0,
|
||||
(slideover_page(a).empty() ? "none" : slideover_page(a).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);
|
||||
(slideover_page(a).empty() ? "none" : slideover_page(a).c_str()), spot.origin.x,
|
||||
spot.origin.y, spot.size.width, spot.size.height, raw.origin.x, raw.origin.y,
|
||||
raw.size.width, raw.size.height, target.origin.x, target.origin.y, target.size.width,
|
||||
target.size.height, covers ? 1 : 0, inside, card.origin.x, card.origin.y,
|
||||
card.size.width, card.size.height,
|
||||
have_card && visible_part && !overlaps(card, target) ? 1 : 0);
|
||||
}
|
||||
|
||||
gboolean helpui_probe_step(gpointer user) {
|
||||
|
||||
Reference in new issue
Block a user