feat: report every annotation under the cursor, not just the topmost
The hover hit-test collected its result into a single int, and each layer assigned unconditionally, so the last box drawn won and everything beneath it was silently dropped. With multiple stations transmitting concurrently that hid most of what was there: co-located frames in the same band are indistinguishable on screen, and the tooltip would name exactly one of them with no indication the others existed. Collect the hits into a stack during the same draw pass (the collision math was already there, only the result was being discarded). When more than one box is under the cursor the tooltip becomes a list — one row per event, topmost-first, each with its own colour swatch so a row maps back to a box on screen. Rows lead with node / frame / seq / ch, which is what actually separates overlapping transmissions. Depth is capped at MAX_HOVER_STACK with a "+N more" count so a deep pile can't cover the spectrogram; a single hit still gets the full-detail tooltip as before. Clicking a stack pins the list so it can be read without holding the cursor still — otherwise the tooltip vanishes the moment you move toward it. Click again or Esc to release. A pinned stack is a frozen snapshot, so live hover does not overwrite it; it is dropped when the annotation overlay is hidden or a new file is loaded, both of which would leave it pointing at events that no longer exist. Clicking stacked annotations pins rather than clearing the selection. A click on empty space or a lone box clears as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
This commit is contained in:
@@ -232,6 +232,9 @@ void ResetForNewSignal(void)
|
||||
app.hoveredEvent = -1;
|
||||
app.hoveredTimelineEvent = -1;
|
||||
app.selectedAnnotation = -1;
|
||||
// Indices point into the events array we just freed.
|
||||
app.hoverStackCount = 0;
|
||||
app.hoverStackPinned = false;
|
||||
app.autocropPending = true; // run once when this file's STFT is ready
|
||||
}
|
||||
|
||||
@@ -835,6 +838,8 @@ int main(int argc, char* argv[])
|
||||
app.timelineExpanded = false;
|
||||
app.hoveredTimelineEvent = -1;
|
||||
app.selectedAnnotation = -1;
|
||||
app.hoverStackCount = 0;
|
||||
app.hoverStackPinned = false;
|
||||
for (int i = 0; i < MLNL_KIND_MAX; i++) app.annotationKindEnabled[i] = true;
|
||||
app.showScope = true;
|
||||
app.dividerY = 0.6f; // Start with 60% spectro, 40% scope
|
||||
@@ -1164,6 +1169,10 @@ int main(int argc, char* argv[])
|
||||
app.showAbout = false;
|
||||
} else if (app.showFileBrowser) {
|
||||
app.showFileBrowser = false;
|
||||
} else if (app.hoverStackPinned) {
|
||||
// Release a pinned annotation stack before touching the
|
||||
// selection — it's the most recently opened thing on screen.
|
||||
app.hoverStackPinned = false;
|
||||
} else if (app.markerMode && app.marker.active) {
|
||||
// Clear the marker measurement first when the ruler is active.
|
||||
app.marker.active = false;
|
||||
@@ -1344,6 +1353,13 @@ int main(int argc, char* argv[])
|
||||
app.sel.freqStart = app.sel.freqEnd;
|
||||
app.sel.freqEnd = tmp;
|
||||
}
|
||||
} else if (app.hoverStackPinned || app.hoverStackCount > 1) {
|
||||
// A click on stacked annotations pins (or unpins) the
|
||||
// hit list instead of touching the selection — with
|
||||
// boxes piled up, "what is under here?" is what the
|
||||
// click means. Pinning freezes the stack so it can be
|
||||
// read without holding the cursor perfectly still.
|
||||
app.hoverStackPinned = !app.hoverStackPinned;
|
||||
} else if (!hoverInsideSelection) {
|
||||
// Sub-threshold drag outside any existing selection: treat
|
||||
// as a click on empty space and reset to full range. A
|
||||
|
||||
Reference in New Issue
Block a user