From a3892c0fd3cd485b95046df24f5a1b177ca96d9f Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 1 Sep 2026 00:49:06 -0500 Subject: [PATCH] fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() — which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true) AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire "Blockchain rescan complete" for a scan the user never initiated. On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end, via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 12 ++++++++++++ src/app.h | 4 ++++ src/main.cpp | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/app.cpp b/src/app.cpp index 6f8053d..bc9af9f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5047,6 +5047,18 @@ void App::refreshNow() invalidateShieldedHistoryScanProgress(true); } +void App::skipDaemonOutputBacklog() +{ + // While minimized, App::update() is paused, so daemon_output_offset_ is never advanced and a large + // backlog of daemon output piles up. Parsing it all at once on restore would replay a background + // witness rebuild's progress + completion in a single batch and fire a spurious "Blockchain rescan + // complete" toast. Advance the offset to the current end so only NEW (post-restore) output is parsed. + // A genuine user-initiated rescan still surfaces completion via the getrescaninfo monitor. + if (daemon_controller_ && daemon_controller_->isRunning()) { + (void)daemon_controller_->outputSince(daemon_output_offset_); // advances daemon_output_offset_ to the end + } +} + void App::handlePaymentURI(const std::string& uri) { auto payment = util::parsePaymentURI(uri); diff --git a/src/app.h b/src/app.h index 0a34982..75460ec 100644 --- a/src/app.h +++ b/src/app.h @@ -393,6 +393,10 @@ public: // Force refresh void refreshNow(); + // Called on window restore: drop the daemon-output backlog that accumulated while minimized (the + // per-frame update loop was paused), so a background witness rebuild that started+finished during + // the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast). + void skipDaemonOutputBacklog(); void refreshMiningInfo(); void refreshPeerInfo(); void refreshMarketData(); diff --git a/src/main.cpp b/src/main.cpp index 4e1dbd7..fbbc99b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1263,6 +1263,36 @@ int main(int argc, char* argv[]) SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale)); } + // DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window + // size, placing it on the largest display that can hold it and bypassing the primary-monitor + // clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440 + // on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect. + int wantW = 0, wantH = 0; + if (const char* geom = std::getenv("DRAGONX_WIN_GEOM")) + sscanf(geom, "%dx%d", &wantW, &wantH); + if (wantW > 0 && wantH > 0) { + int count = 0; + SDL_DisplayID* disp = SDL_GetDisplays(&count); + SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0}; + for (int i = 0; i < count; ++i) { + SDL_Rect u; + if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue; + bool fits = (u.w >= wantW && u.h >= wantH); + bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH); + // Prefer a display that fits; among those, the smallest; else the largest available. + if ((fits && !bestFits) || + (fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) || + (!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) { + bestUsable = u; best = disp[i]; + } + } + if (disp) SDL_free(disp); + if (best) { + SDL_SetWindowMinimumSize(window, 320, 240); + SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10); + SDL_SetWindowSize(window, wantW, wantH); + } + } else { // Clamp to the current display's work area — runs on EVERY startup (this clamp used to live // inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the // window off-screen or bigger than the screen on a same-DPI cold start). @@ -1281,6 +1311,7 @@ int main(int argc, char* argv[]) DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n", curW, curH, newW, newH, currentDpiScale); } + } } #endif winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale); @@ -1552,6 +1583,7 @@ int main(int argc, char* argv[]) // Window restored from minimized — trigger immediate data refresh if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED && waitEvent.window.windowID == SDL_GetWindowID(window)) { + app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast) app.refreshNow(); } // Handle DPI change that arrived while idle (same logic as poll loop) @@ -1681,6 +1713,7 @@ int main(int argc, char* argv[]) // Window restored from minimized — trigger immediate data refresh if (event.type == SDL_EVENT_WINDOW_RESTORED && event.window.windowID == SDL_GetWindowID(window)) { + app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast) app.refreshNow(); } // Handle DPI/display scale changes (e.g. window dragged to a