Skip to content

Add crosshair-link toggle to FastSense dashboard widgets (260602-mri)#184

Merged
HanSur94 merged 6 commits into
mainfrom
claude/elegant-ride-765542
Jun 2, 2026
Merged

Add crosshair-link toggle to FastSense dashboard widgets (260602-mri)#184
HanSur94 merged 6 commits into
mainfrom
claude/elegant-ride-765542

Conversation

@HanSur94
Copy link
Copy Markdown
Owner

@HanSur94 HanSur94 commented Jun 2, 2026

Summary

Adds a crosshair-link toggle (X) to each FastSense dashboard widget's grey WidgetButtonBar (left of the V/A buttons). When enabled on 2+ FastSense widgets on the active page, moving the hover crosshair over any one of them mirrors the crosshair's data-x onto all the others — each showing its own per-series datatip at that x, so you can compare values at the same time/x across plots. Toggle off to unlink. Default OFF; legacy serialized dashboards load byte-identical.

Quick task 260602-mri (routed via /gsd:do/gsd:quick).

How it works

The mirror rides on the existing per-crosshair onMove/onLeave chain — HoverCrosshair.onMove broadcasts the data-x via BroadcastFcn_, peers' onMoveExternal re-show at that x, and the existing computeYAtX_ gives each peer its own Y for free (no Y transmitted, raw-x). Zero new figure-WindowButtonMotionFcn closures — the constraint that prevents repeating the 260512-egv/eu2 chained-WBM regression. A peer being mirrored is held visible by a deterministic IsMirrored_ flag (its same-dispatch self-leave no-ops); the hover source always broadcasts leave on cursor-exit, and onLeaveExternal hides peers directly (no leave recursion).

DashboardEngine derives the active-page link set on demand (collectLinkedCrosshairs_, flattening GroupWidget children) and re-primes the broadcast hooks after rerenderWidgets / switchPage / detachWidget. The X button is duck-typed into DashboardLayout.realizeWidget via ismethod(widget,'setCrosshairLink') (same pattern as the V/A/L buttons), re-anchored by reflowChrome_, and protected in clearPanelControls.

Files

File Change
libs/FastSense/HoverCrosshair.m setBroadcastFcn/onMoveExternal/onLeaveExternal, deterministic IsMirrored_ suppress, hideGraphics_ helper, broadcast tail in onMove
libs/Dashboard/FastSenseWidget.m CrosshairLinked property + setCrosshairLink + toStruct/fromStruct (omit-when-false)
libs/Dashboard/DashboardEngine.m active-page link coordination + lifecycle re-wiring
libs/Dashboard/DashboardLayout.m duck-typed X button + reflow re-anchor
libs/Dashboard/DashboardWidget.m CrosshairLinkButton added to clearPanelControls protected tags
tests/test_fastsense_crosshair_link.m new — 11 cases

Verification (MATLAB R2025a, live)

  • test_fastsense_crosshair_link 11/11 (incl. deterministic suppress-leave crux + 2-widget mirror integration)
  • Regressions green: test_hover_crosshair 11/11 · test_fastsense_widget_ylimit_modes 11/11 · test_time_range_selector_reinstall_after_rerender pass (chained-WBM guard) · test_dashboard_time_sync_all_pages 5/5
  • MISS_HIT mh_style+mh_lint clean on all 6 files; Code Analyzer no new findings
  • Live UI smoke on the industrial-plant demo: button renders + toggles, hovering one linked widget mirrors crosshair+datatip on the other, leave hides both, unlink stops mirroring

Orchestrator verification caught and fixed two defects in the initial implementation: a flaky wall-clock suppress window (→ deterministic IsMirrored_) and a latent unbounded leave ping-pong (→ onLeaveExternal hides directly).

Follow-up

The new test is function-based, so it runs locally + on the Octave job but not in the MATLAB CI coverage shards (those load tests/suite/Test*.m only) → 0 Codecov patch coverage in those shards. A class-based tests/suite/TestFastSenseCrosshairLink.m is recommended as a follow-up.

Out of scope

DetachedMirror crosshair-link parity — detached widgets use a figure-level FastSenseToolbar, not a WidgetButtonBar (matches the 260513-sfp detached V/A/L precedent).

🤖 Generated with Claude Code

HanSur94 and others added 6 commits June 2, 2026 16:38
…ast hook

- FastSenseWidget: add CrosshairLinked=false public property, setCrosshairLink(tf)
  setter (validates logical/0/1, throws FastSenseWidget:invalidCrosshairLink),
  toStruct omits field when false (legacy JSON byte-identical), fromStruct
  restores flag pre-render (no graphics touch)
- HoverCrosshair: add BroadcastFcn_/BroadcastLeaveFcn_ callbacks + setBroadcastFcn;
  onMoveExternal(x) sets SuppressLeaveUntil_=tic then drives onMove with
  InBroadcast_=true; onLeaveExternal() clears suppress + hides; onMove fires
  BroadcastFcn_ at tail; onLeave honors SuppressWindow_ guard and fires
  BroadcastLeaveFcn_ when source leaves; delete() nulls callbacks first
- tests/test_fastsense_crosshair_link.m: 11 cases (6 PURE + 1 pure-engine +
  2 render-guarded suppress-leave proof + broadcast-reentry proof +
  2-widget mirror integration)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ination

- collectLinkedCrosshairs_(widgets): pure flattening helper (public);
  returns cell of {widget,hc} structs for linked+rendered FastSenseWidgets
- rewireCrosshairLinks_(): clears all active-page broadcast hooks then
  re-primes linked crosshairs with engine broadcast closures
- broadcastCrosshairX_(sourceHc, x): mirrors data-x onto all OTHER linked
  crosshairs via onMoveExternal
- broadcastCrosshairLeave_(sourceHc): tells all OTHER linked crosshairs to
  hide via onLeaveExternal
- onCrosshairLinkToggle(widget): re-derives full active-page link set
- rewireCrosshairLinks_ called after rerenderWidgets, switchPage, detachWidget
  (260512-eu2 re-establish-after-rerender lesson)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DashboardLayout.realizeWidget: inject CrosshairLinkButton via
  ismethod(widget,'setCrosshairLink') duck-type after addPlantLogToggle,
  before final reflowChrome_
- addCrosshairLinkToggle(widget): idempotent 24x24 'X' pushbutton;
  highlighted when CrosshairLinked=true via chooseYLimitActiveBg_;
  calls onCrosshairLinkTogglePressed_ on click; tails with reflowChrome_
  for callback-driven rebuilds
- onCrosshairLinkTogglePressed_: toggles CrosshairLinked, calls
  EngineRef.onCrosshairLinkToggle, rebuilds button look; try/catch +
  warning + non-blocking uialert mirrors addPlantLogToggle pattern
- reflowChrome_: re-anchors CrosshairLinkButton as LEFTMOST chrome button
  (xVisible - gap - bw, left of V/A cluster) after resize
- DashboardWidget.clearPanelControls: add 'CrosshairLinkButton' to
  protectedTags so button survives re-render sweeps

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ursion fix

Orchestrator live-MATLAB verification found the suppress-leave tic-window
(SuppressLeaveUntil_/SuppressWindow_) was flaky: it depends on wall-clock
elapsed time inside a synchronous motion dispatch, so the crux test passed
in isolation (6.7ms) but failed in the full suite. Replaced with a
deterministic IsMirrored_ boolean - set by onMoveExternal, cleared when the
widget becomes the hover source (real onMove) or via onLeaveExternal.

Also fixes a latent unbounded leave ping-pong: onLeaveExternal now hides
directly via a new private hideGraphics_ helper instead of re-entering the
broadcasting onLeave (which would recurse across >=2 linked widgets and hang).

Test fixes: collectLinkedCrosshairs_ enumeration passes the widget list as a
parameter, and the 2-widget integration test uses addWidget(), since
DashboardEngine.Widgets is SetAccess=private.

Verified R2025a: test_fastsense_crosshair_link 11/11; regressions green
(test_hover_crosshair 11/11, test_fastsense_widget_ylimit_modes 11/11,
test_time_range_selector_reinstall_after_rerender pass,
test_dashboard_time_sync_all_pages 5/5); MISS_HIT clean; live UI smoke pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Quick task 260602-mri complete and verified (R2025a live MATLAB):
a crosshair-link 'X' toggle on the FastSense widget grey bar mirrors the
hover crosshair across all FastSense widgets on the active dashboard page.

- PLAN.md: the executable plan (3 tasks) — now tracked
- SUMMARY.md: updated for the deterministic IsMirrored_ suppress redesign,
  the latent leave-recursion fix, and the green test/lint/live-smoke results
- STATE.md: Quick Tasks Completed row + last-activity line

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HanSur94 HanSur94 merged commit 87015be into main Jun 2, 2026
4 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 36.41975% with 103 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/Dashboard/DashboardEngine.m 39.74% 47 Missing ⚠️
libs/FastSense/HoverCrosshair.m 0.00% 28 Missing ⚠️
libs/Dashboard/DashboardLayout.m 55.31% 21 Missing ⚠️
libs/Dashboard/FastSenseWidget.m 22.22% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

HanSur94 added a commit that referenced this pull request Jun 3, 2026
Resolve conflicts from main's Dashboard API hardening (#178) and
crosshair-link toggle (#184):
- FastSenseWidget.m: keep all three methods -- 1041's setTimeWindow +
  isShowingEmptyState alongside main's setCrosshairLink (independent
  additions at the same location; added the missing end for
  isShowingEmptyState). All three backing properties coexist.
- .planning/STATE.md: status/activity lines reconciled to current state.

Merge verified locally: TestFastSenseWidget 16/16, test_dashboard_time_window
8/8, TestCompanionTimeBar 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant