From 2517ed0b2971a2685f3c5047daf4eb417f3e5894 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Tue, 17 Mar 2026 23:11:10 +0100 Subject: [PATCH] [fix](trx-frontend): fix spectrum screenshot hotkey Preserve the WebGL drawing buffers used by the spectrum snapshot, flush them before compositing, and move the shortcut listener to capture phase so focused widgets do not swallow it. Co-authored-by: OpenAI Codex Signed-off-by: Stanislaw Grams --- .../trx-frontend-http/assets/web/app.js | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index f2ee438..09afd9e 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -339,11 +339,13 @@ const connLostOverlayTitleEl = document.getElementById("conn-lost-overlay-title" const connLostOverlaySubEl = document.getElementById("conn-lost-overlay-sub"); const overviewCanvas = document.getElementById("overview-canvas"); const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); +// Screenshots composite these live WebGL canvases into a PNG. +const spectrumSnapshotGlOptions = { alpha: true, preserveDrawingBuffer: true }; const overviewGl = typeof createTrxWebGlRenderer === "function" - ? createTrxWebGlRenderer(overviewCanvas, { alpha: true }) + ? createTrxWebGlRenderer(overviewCanvas, spectrumSnapshotGlOptions) : null; const signalOverlayGl = typeof createTrxWebGlRenderer === "function" - ? createTrxWebGlRenderer(signalOverlayCanvas, { alpha: true }) + ? createTrxWebGlRenderer(signalOverlayCanvas, spectrumSnapshotGlOptions) : null; const signalVisualBlockEl = document.querySelector(".signal-visual-block"); const signalSplitControlEl = document.getElementById("signal-split-control"); @@ -7920,7 +7922,7 @@ window.addEventListener("beforeunload", () => { // ── Spectrum display ───────────────────────────────────────────────────────── const spectrumCanvas = document.getElementById("spectrum-canvas"); const spectrumGl = typeof createTrxWebGlRenderer === "function" - ? createTrxWebGlRenderer(spectrumCanvas, { alpha: true }) + ? createTrxWebGlRenderer(spectrumCanvas, spectrumSnapshotGlOptions) : null; const spectrumDbAxis = document.getElementById("spectrum-db-axis"); const spectrumFreqAxis = document.getElementById("spectrum-freq-axis"); @@ -9103,6 +9105,16 @@ function buildSpectrumSnapshotCanvas() { if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) { return null; } + for (const renderer of [overviewGl, spectrumGl, signalOverlayGl]) { + const gl = renderer?.gl; + if (!gl) continue; + try { + if (typeof gl.flush === "function") gl.flush(); + if (typeof gl.finish === "function") gl.finish(); + } catch (_) { + // Ignore transient WebGL state errors and capture the last good frame. + } + } const rootRect = rootEl.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const out = document.createElement("canvas"); @@ -9153,35 +9165,55 @@ function buildSpectrumSnapshotCanvas() { return out; } -function saveCanvasAsPng(canvas, fileName) { - if (!canvas) return; - if (typeof canvas.toBlob === "function") { - canvas.toBlob((blob) => { - if (!blob) return; - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = fileName; - a.click(); - setTimeout(() => URL.revokeObjectURL(url), 1000); - }, "image/png"); - return; - } +function clickCanvasDownload(href, fileName) { const a = document.createElement("a"); - a.href = canvas.toDataURL("image/png"); + a.href = href; a.download = fileName; + a.rel = "noopener"; + a.style.display = "none"; + document.body.appendChild(a); a.click(); + requestAnimationFrame(() => a.remove()); } -function captureSpectrumScreenshot() { +function saveCanvasAsPng(canvas, fileName) { + if (!canvas) return Promise.resolve(false); + if (typeof canvas.toBlob === "function") { + return new Promise((resolve) => { + try { + canvas.toBlob((blob) => { + if (!blob) { + resolve(false); + return; + } + const url = URL.createObjectURL(blob); + clickCanvasDownload(url, fileName); + setTimeout(() => URL.revokeObjectURL(url), 1000); + resolve(true); + }, "image/png"); + } catch (_) { + resolve(false); + } + }); + } + try { + clickCanvasDownload(canvas.toDataURL("image/png"), fileName); + return Promise.resolve(true); + } catch (_) { + return Promise.resolve(false); + } +} + +async function captureSpectrumScreenshot() { const snapshotCanvas = buildSpectrumSnapshotCanvas(); if (!snapshotCanvas) { showHint("Spectrum view not ready", 1300); - return; + return false; } const stamp = new Date().toISOString().replace(/[:.]/g, "-"); - saveCanvasAsPng(snapshotCanvas, `trx-spectrum-${stamp}.png`); - showHint("Spectrum screenshot saved", 1500); + const saved = await saveCanvasAsPng(snapshotCanvas, `trx-spectrum-${stamp}.png`); + showHint(saved ? "Spectrum screenshot saved" : "Spectrum screenshot failed", saved ? 1500 : 1800); + return saved; } function shouldIgnoreGlobalShortcut(target) { @@ -9193,13 +9225,13 @@ function shouldIgnoreGlobalShortcut(target) { } window.addEventListener("keydown", (event) => { - if (event.defaultPrevented || event.repeat) return; + if (event.defaultPrevented || event.repeat || event.isComposing) return; if (event.ctrlKey || event.metaKey || event.altKey) return; if (shouldIgnoreGlobalShortcut(event.target)) return; if ((event.key || "").toLowerCase() !== "s") return; event.preventDefault(); - captureSpectrumScreenshot(); -}); + void captureSpectrumScreenshot(); +}, { capture: true }); // ── Zoom helpers ────────────────────────────────────────────────────────────── function spectrumZoomAt(cssX, cssW, data, factor) {