feat: hide control annotations by default, demote auto-crop notice to a toast
Control events are zero-duration log markers about the run rather than signals on the air. A busy capture carries thousands, and they clutter the overlay without saying anything about what was transmitted, so the GUI now starts with that kind hidden; the per-kind checkbox brings it back. The headless --render path still enables every kind — an export should render what was asked for, not a GUI preference. The auto-crop notice was a full modal: it dimmed the window, took focus, and demanded a click before the user could look at the file they had just opened. Auto-crop is a helpful default, not a decision worth blocking on. It is now a bottom-right toast offering Undo / Dismiss, with a progress strip showing the remaining time so it doesn't just vanish mid-read, and it is out of UiModalOpen() so it no longer swallows keys or blocks the spectrogram underneath. The countdown only advances while the window is focused, so a crop applied during a background load is still readable when the user returns. The per-frame step is clamped: auto-crop fires on the frame right after the blocking STFT compute, and GetFrameTime() there reports the entire compute, which drained the whole 5 s budget at once and made the toast flash by in an instant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
This commit is contained in:
@@ -147,6 +147,7 @@ static bool IsAppActive(void)
|
|||||||
if (app.sel.isDragging || app.sel.isTimeSelecting || app.sel.isFreqSelecting) return true;
|
if (app.sel.isDragging || app.sel.isTimeSelecting || app.sel.isFreqSelecting) return true;
|
||||||
if (app.marker.dragging) return true;
|
if (app.marker.dragging) return true;
|
||||||
if (app.exportMessageTimer > 0.0f) return true; // notification countdown
|
if (app.exportMessageTimer > 0.0f) return true; // notification countdown
|
||||||
|
if (app.autocropNoticeActive) return true; // toast countdown + progress strip
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +527,7 @@ void ApplyAutoCrop(void)
|
|||||||
timePart,
|
timePart,
|
||||||
srcPart);
|
srcPart);
|
||||||
app.autocropNoticeActive = true;
|
app.autocropNoticeActive = true;
|
||||||
|
app.autocropNoticeTimer = AUTOCROP_NOTICE_SECONDS;
|
||||||
TraceLog(LOG_INFO, "Auto-crop: %s", app.autocropNoticeMsg);
|
TraceLog(LOG_INFO, "Auto-crop: %s", app.autocropNoticeMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,6 +970,13 @@ int main(int argc, char* argv[])
|
|||||||
app.hoverStackCount = 0;
|
app.hoverStackCount = 0;
|
||||||
app.currentCollision = -1;
|
app.currentCollision = -1;
|
||||||
for (int i = 0; i < MLNL_KIND_MAX; i++) app.annotationKindEnabled[i] = true;
|
for (int i = 0; i < MLNL_KIND_MAX; i++) app.annotationKindEnabled[i] = true;
|
||||||
|
// Control events are zero-duration log markers about the run, not signals on
|
||||||
|
// the air. On a busy capture there are thousands of them and they clutter
|
||||||
|
// the overlay without saying anything about what was transmitted, so they
|
||||||
|
// start hidden; the per-kind checkbox turns them back on. (Headless
|
||||||
|
// --render keeps every kind enabled — an export should show what was asked
|
||||||
|
// for, not a GUI default.)
|
||||||
|
app.annotationKindEnabled[MLNL_KIND_CONTROL] = false;
|
||||||
app.showScope = true;
|
app.showScope = true;
|
||||||
app.dividerY = 0.6f; // Start with 60% spectro, 40% scope
|
app.dividerY = 0.6f; // Start with 60% spectro, 40% scope
|
||||||
app.isDividing = false;
|
app.isDividing = false;
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ typedef struct {
|
|||||||
// still reports the total collision count, it just stops adding bands.
|
// still reports the total collision count, it just stops adding bands.
|
||||||
#define MAX_COLLISION_REGIONS 4096
|
#define MAX_COLLISION_REGIONS 4096
|
||||||
|
|
||||||
|
// How long the auto-crop toast stays up, in seconds of *focused* time.
|
||||||
|
#define AUTOCROP_NOTICE_SECONDS 5.0f
|
||||||
|
|
||||||
// How many overlapping annotation boxes the cursor-hit stack retains. Deeper
|
// How many overlapping annotation boxes the cursor-hit stack retains. Deeper
|
||||||
// piles than this are counted but not listed individually (the tooltip says
|
// piles than this are counted but not listed individually (the tooltip says
|
||||||
// "+N more"), which keeps a dense pile-up from covering the spectrogram.
|
// "+N more"), which keeps a dense pile-up from covering the spectrogram.
|
||||||
@@ -306,6 +309,7 @@ typedef struct {
|
|||||||
// user dismisses with "OK" (keep crop) or "Uncrop" (restore full view).
|
// user dismisses with "OK" (keep crop) or "Uncrop" (restore full view).
|
||||||
bool autocropNoticeActive;
|
bool autocropNoticeActive;
|
||||||
char autocropNoticeMsg[256];
|
char autocropNoticeMsg[256];
|
||||||
|
float autocropNoticeTimer; // seconds left; counts down only while focused
|
||||||
|
|
||||||
// Optional mLnL annotations parsed from the loaded WAV (empty if the file
|
// Optional mLnL annotations parsed from the loaded WAV (empty if the file
|
||||||
// doesn't carry the chunk). The annotations overlay has two surfaces:
|
// doesn't carry the chunk). The annotations overlay has two surfaces:
|
||||||
@@ -381,7 +385,9 @@ void ApplyAutoCrop(void);
|
|||||||
// is gated off while this is the case. Add new overlays here in one place.
|
// is gated off while this is the case. Add new overlays here in one place.
|
||||||
static inline bool UiModalOpen(void)
|
static inline bool UiModalOpen(void)
|
||||||
{
|
{
|
||||||
return app.showFileBrowser || app.showAbout || app.autocropNoticeActive;
|
// The auto-crop notice is a toast, not a modal — it must not swallow keys
|
||||||
|
// or block interaction with the spectrogram underneath it.
|
||||||
|
return app.showFileBrowser || app.showAbout;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the box selection to the full signal (the "no selection" state).
|
// Reset the box selection to the full signal (the "no selection" state).
|
||||||
|
|||||||
@@ -826,6 +826,13 @@ void DrawAboutDialog(void)
|
|||||||
// [OK] — dismiss; keep the cropped view
|
// [OK] — dismiss; keep the cropped view
|
||||||
// Esc closes (= OK). Clicks outside the panel do nothing (avoids losing the
|
// Esc closes (= OK). Clicks outside the panel do nothing (avoids losing the
|
||||||
// crop by missing a button by a few px).
|
// crop by missing a button by a few px).
|
||||||
|
// Auto-crop notice, as a bottom-right toast rather than a modal.
|
||||||
|
//
|
||||||
|
// Auto-crop is a helpful default, not a decision worth blocking on: a modal
|
||||||
|
// stole focus and demanded a click before the user could look at the file they
|
||||||
|
// had just opened. The toast states what happened, offers Undo, and expires on
|
||||||
|
// its own — but only counts down while the window is focused, so a crop applied
|
||||||
|
// during a background load is still there to read when the user comes back.
|
||||||
void DrawAutocropNotice(void)
|
void DrawAutocropNotice(void)
|
||||||
{
|
{
|
||||||
if (!app.autocropNoticeActive) return;
|
if (!app.autocropNoticeActive) return;
|
||||||
@@ -833,52 +840,52 @@ void DrawAutocropNotice(void)
|
|||||||
int sw = GetScreenWidth();
|
int sw = GetScreenWidth();
|
||||||
int sh = GetScreenHeight();
|
int sh = GetScreenHeight();
|
||||||
|
|
||||||
DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.55f));
|
const char* title = "Auto-crop applied";
|
||||||
|
float titleW = MeasureTextScaled(title, 13);
|
||||||
|
float msgW = MeasureTextScaled(app.autocropNoticeMsg, 11);
|
||||||
|
float btnW = 76 * scale, btnH = 24 * scale;
|
||||||
|
|
||||||
float pw = 460 * scale;
|
float pw = fmaxf(fmaxf(titleW, msgW) + 28 * scale, btnW * 2 + 44 * scale);
|
||||||
float ph = 170 * scale;
|
float ph = 92 * scale;
|
||||||
Rectangle panel = { (sw - pw) * 0.5f, (sh - ph) * 0.5f, pw, ph };
|
float margin = 16 * scale;
|
||||||
DrawRectangleRec(panel, (Color){ 30, 30, 40, 255 });
|
Rectangle panel = { sw - pw - margin, sh - ph - margin, pw, ph };
|
||||||
DrawRectangleLinesEx(panel, 2, (Color){ 160, 130, 200, 255 });
|
|
||||||
|
|
||||||
DrawTextScaled("Auto-crop applied",
|
DrawRectangleRec(panel, (Color){ 28, 28, 36, 240 });
|
||||||
panel.x + 20 * scale, panel.y + 16 * scale, 16,
|
DrawRectangleLinesEx(panel, 1, (Color){ 150, 120, 190, 255 });
|
||||||
|
|
||||||
|
DrawTextScaled(title, panel.x + 12 * scale, panel.y + 10 * scale, 13,
|
||||||
(Color){ 200, 170, 240, 255 });
|
(Color){ 200, 170, 240, 255 });
|
||||||
|
DrawTextScaled(app.autocropNoticeMsg, panel.x + 12 * scale, panel.y + 30 * scale,
|
||||||
|
11, LIGHTGRAY);
|
||||||
|
|
||||||
// Body: word-wrap not needed for the short message ApplyAutoCrop builds.
|
float btnY = panel.y + ph - btnH - 10 * scale;
|
||||||
DrawTextScaled(app.autocropNoticeMsg,
|
Rectangle undoBtn = { panel.x + 12 * scale, btnY, btnW, btnH };
|
||||||
panel.x + 20 * scale, panel.y + 50 * scale, 13, LIGHTGRAY);
|
Rectangle okBtn = { panel.x + pw - btnW - 12 * scale, btnY, btnW, btnH };
|
||||||
|
|
||||||
float btnW = 110 * scale, btnH = 32 * scale;
|
bool undoHover = CheckCollisionPointRec(GetMousePosition(), undoBtn);
|
||||||
float btnY = panel.y + ph - btnH - 16 * scale;
|
|
||||||
Rectangle uncropBtn = { panel.x + 20 * scale, btnY, btnW, btnH };
|
|
||||||
Rectangle okBtn = { panel.x + pw - btnW - 20*scale, btnY, btnW, btnH };
|
|
||||||
|
|
||||||
bool uncropHover = CheckCollisionPointRec(GetMousePosition(), uncropBtn);
|
|
||||||
bool okHover = CheckCollisionPointRec(GetMousePosition(), okBtn);
|
bool okHover = CheckCollisionPointRec(GetMousePosition(), okBtn);
|
||||||
|
|
||||||
DrawPanelBox(uncropBtn,
|
DrawPanelBox(undoBtn,
|
||||||
uncropHover ? (Color){ 100, 60, 60, 255 } : (Color){ 70, 40, 40, 255 },
|
undoHover ? (Color){ 100, 60, 60, 255 } : (Color){ 70, 40, 40, 255 },
|
||||||
(Color){ 230, 160, 160, 255 });
|
(Color){ 230, 160, 160, 255 });
|
||||||
DrawTextScaled("Uncrop",
|
DrawTextScaled("Undo", undoBtn.x + btnW * 0.5f - MeasureTextScaled("Undo", 12) * 0.5f,
|
||||||
uncropBtn.x + btnW * 0.5f - MeasureTextScaled("Uncrop", 14) * 0.5f,
|
undoBtn.y + 5 * scale, 12, WHITE);
|
||||||
uncropBtn.y + 8 * scale, 14, WHITE);
|
|
||||||
|
|
||||||
DrawPanelBox(okBtn,
|
DrawPanelBox(okBtn,
|
||||||
okHover ? (Color){ 60, 90, 60, 255 } : (Color){ 40, 70, 40, 255 },
|
okHover ? (Color){ 60, 90, 60, 255 } : (Color){ 45, 60, 45, 255 },
|
||||||
(Color){ 160, 220, 160, 255 });
|
(Color){ 160, 200, 160, 255 });
|
||||||
DrawTextScaled("OK",
|
DrawTextScaled("Dismiss", okBtn.x + btnW * 0.5f - MeasureTextScaled("Dismiss", 12) * 0.5f,
|
||||||
okBtn.x + btnW * 0.5f - MeasureTextScaled("OK", 14) * 0.5f,
|
okBtn.y + 5 * scale, 12, WHITE);
|
||||||
okBtn.y + 8 * scale, 14, WHITE);
|
|
||||||
|
|
||||||
DrawTextScaled("Esc or Enter = OK", panel.x + pw - 156 * scale,
|
// Remaining-time strip along the bottom edge, so the auto-expiry is visible
|
||||||
panel.y + ph - 14 * scale, 10, GRAY);
|
// rather than the toast just vanishing mid-read.
|
||||||
|
float frac = app.autocropNoticeTimer / AUTOCROP_NOTICE_SECONDS;
|
||||||
|
if (frac < 0.0f) frac = 0.0f;
|
||||||
|
if (frac > 1.0f) frac = 1.0f;
|
||||||
|
DrawRectangle((int)panel.x, (int)(panel.y + ph - 2 * scale),
|
||||||
|
(int)(pw * frac), (int)(2 * scale), (Color){ 150, 120, 190, 200 });
|
||||||
|
|
||||||
// Hit handling. Esc/Enter dismiss (keep crop). Uncrop restores full view.
|
if (undoHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||||
if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_ENTER)) {
|
|
||||||
app.autocropNoticeActive = false;
|
|
||||||
}
|
|
||||||
if (uncropHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
|
||||||
app.displayMaxFreqHz = 0.0f;
|
app.displayMaxFreqHz = 0.0f;
|
||||||
app.view.start = 0.0f; app.view.end = 1.0f;
|
app.view.start = 0.0f; app.view.end = 1.0f;
|
||||||
app.view.freqStart = 0.0f; app.view.freqEnd = 1.0f;
|
app.view.freqStart = 0.0f; app.view.freqEnd = 1.0f;
|
||||||
@@ -887,4 +894,18 @@ void DrawAutocropNotice(void)
|
|||||||
} else if (okHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
} else if (okHover && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||||
app.autocropNoticeActive = false;
|
app.autocropNoticeActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count down only while focused: a toast that expired behind another window
|
||||||
|
// would be gone before it was ever seen.
|
||||||
|
//
|
||||||
|
// The step is clamped because auto-crop fires on the frame right after the
|
||||||
|
// blocking STFT compute, and GetFrameTime() on that frame reports the whole
|
||||||
|
// compute — seconds on a long capture. Unclamped, that single frame drained
|
||||||
|
// the entire 5 s budget and the toast flashed by in an instant.
|
||||||
|
if (IsWindowFocused()) {
|
||||||
|
float dt = GetFrameTime();
|
||||||
|
if (dt > 0.1f) dt = 0.1f; // ignore load hitches / debugger stalls
|
||||||
|
app.autocropNoticeTimer -= dt;
|
||||||
|
if (app.autocropNoticeTimer <= 0.0f) app.autocropNoticeActive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user