Skip to content

Commit 3b3312e

Browse files
rfoustten9876claude
authored
Add clock-aligned waterfall time markers (#5538)
## Summary Closes #5537. Right-click the panadapter or waterfall and choose **Waterfall Time Markers** to display thin horizontal lines with small UTC timestamps at the left edge. Intervals are Off, 15/30 seconds, and 1/5/10/15 minutes. Off remains the default, and the selection persists independently per panadapter slot in the existing Display document. Markers align to clock boundaries (for example, HH:mm:00 for one-minute markers) and follow their captured signal rows through live scrolling, paused history, and resize. Changing the waterfall rate affects future row cadence; existing history retains its captured row spacing. GPU and software rendering share timestamp geometry and a cached label atlas. Dedicated theme tokens provide subdued pale gray-blue lines/text with a dark label backing. This is a client display feature with no radio protocol, TX, dependency, or unrelated behavior changes. Final visual/UX acceptance is requested from the maintainer. The minimum interval is intentionally 15 seconds to limit clutter. ## Review follow-up (maintainer-requested) Four review nits from #5538 are addressed in `Address review nits on waterfall time markers`: - **30-minute and 1-hour intervals retired.** Issue #5537 proposed nine intervals, but `kWaterfallHistoryMs` caps retained history at 20 minutes and a screenful of waterfall is only ~11-19 s at typical rates — measured on the demo simulator at 230-410 rows and 40-47 ms/row. A 1-hour marker would be on screen roughly 0.4% of the time and a 30-minute one ~0.9%. The menu now tops out at 15 minutes. This is a deliberate deviation from the issue text, agreed with the maintainer. A value written by an earlier build fails closed to Off through `validWaterfallMarkerInterval()`; the settings test pins that. - **`waterfallTimeMarkerSeconds` Q_PROPERTY is now read-only.** Nothing needs to set the interval by reflection, and the `dss snapshot` field already carries it for reads. - **The new `dss snapshot` fields are documented** in `docs/automation-bridge.md` next to the sibling DSS fields, with the two assertions that make markers checkable from the bridge. - **The `<=` clock-advance guard's overlap with the bucket comparison is now a deliberate, commented choice**, and the zero-advance row is pinned directly in the geometry test. Rebased onto current `main` (the only conflict was both sides appending to `AETHER_SETTINGS_CONSUMERS` in `tests/tests.cmake`) and the touchpoint manifest regenerated for current main. Independently verified on **Linux** (`AETHER_GPU_SPECTRUM=ON`), the platform the original test plan did not cover, driving the demo simulator over the automation bridge: markers land exactly on clock boundaries, scroll at `1000/msPerRow` px/s (measured 20.82 vs 21.09 expected), survive resize (410→350→230 rows), history scrub, `sim stallscope`/`dropslice`/`malformed`, and disconnect/reconnect, and the interval persists across a process restart. Mutation testing killed 5 of 6 deliberate reversions of the geometry. ## Constitution principle honored - Principle V: preference lives in the existing nested Display settings document, without a new flat settings key. - Principle IV: original Qt implementation; WSJT-X is a behavioral reference only, with no proprietary binary-derived code. - Principle XI: demonstrated via focused tests and native Demo automation-bridge checks. ## Test plan - [x] Native macOS ARM64 RelWithDebInfo build with the local toolchain and RADE enabled; ARM64 executable verified and RNNoise x86 sources absent from the build graph. - [x] Four focused CTests passed: `waterfall_time_markers_test`, `waterfall_time_marker_settings_test`, `waterfall_history_buffer_test`, and `spectrum_preview_logic_test`. - [x] Geometry tests cover all intervals, wrap, fractional scrolling, paused rows, resize mapping, duplicate timestamps, missing timestamps, gaps, backward clock movement, Off, and invalid values. Settings test checks independent slots, sibling preservation, and reload. - [x] Mutation checks: reversing fractional-scroll direction fails the geometry checks; reverting clock-boundary labeling fails delayed-row assertions for every interval. - [x] Native Cocoa/Metal automation bridge using isolated settings, DEMO-0001, and TX disabled: menu selections, live motion, paused history, resize, themes, Off, and restart persistence. A retained marker stayed at row 226 when the paused waterfall grew from 410 to 470 rows. Corrected timestamps were asserted divisible by the selected 15000 ms interval and rendered pixels inspected. - [x] Theme seed, touchpoint manifest, test registration, engine-boundary strict check, and whitespace checks passed. No increase in the hardcoded-color ratchet. - [ ] Windows/Linux GPU runtime verification and actual Flex/Kiwi source-switching validation have not been performed. Software renderer was syntax-checked but not exercised as a full runtime build. - [ ] CI results pending the PR run. New CTests are socket-free. Bridge proof is a separate manual Demo session. No live-radio validation is claimed; reopening the earlier testing profile briefly auto-connected to a saved FLEX, so that instance was closed without transmit and the final handoff used the clean Demo profile. ## Checklist - [x] Commit is SSH-signed and locally verified. - [x] No new flat-key AppSettings calls. - [x] Clean-room code. - [x] Theme token documentation and generated inventories updated; CHANGELOG unchanged. - [x] Local code review completed; identified packet-arrival timestamp labeling corrected before this PR. Prepared with Codex assistance. Meter smoothing and GHSA references are not applicable to this display-only feature. --------- Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a3d1971 commit 3b3312e

15 files changed

Lines changed: 592 additions & 2 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ set(GUI_SOURCES
10681068
src/gui/PanadapterMessageOverlay.h
10691069
src/gui/PanadapterRenderScheduler.cpp
10701070
src/gui/SpectrumWidget.cpp
1071+
src/gui/SpectrumWidget_TimeMarkers.cpp
10711072
src/gui/WaterfallHistoryBuffer.cpp
10721073
src/gui/SpectrumOverlayMenu.cpp
10731074
src/gui/SpectrumOverlayWheelGuard.cpp

docs/architecture/aetherd-touchpoints.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
142142
| `core/SystemInventory.h` | 1 | ui-support — Startup and support-bundle inventory of host CPU, SIMD and RAM capabilities. Process diagnostics, not radio state. | unconverted |
143143
| `core/TciServer.h` | 3 | mixed(flex) — TCI WebSocket server for WSJT-X et al: protocol surface is canonical radio state, but audio/IQ rides Flex DAX | unconverted |
144144
| `core/TgxlConnection.h` | 2 | peripheral(4o3a) — Direct TCP client for the 4O3A Tuner Genius XL (port 9010, relay/autotune), reverse-engineered from the 4O3A management app — a standalone accessory transport, not SmartSDR. Not radio-family wire; a peripheral accessory, NOT behind the IRadioBackend radio seam (reclassified from vendor(flex), #4087 follow-up). | unconverted |
145-
| `core/ThemeManager.h` | 146 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
145+
| `core/ThemeManager.h` | 147 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
146146
| `core/ThreadCpuRing.h` | 2 | ui-support — Short host-thread CPU history used by Runtime Monitor peak and sparkline presentation. Diagnostic UI support, not radio state. | unconverted |
147147
| `core/TimeFrameVoter.h` | 1 | universal — Shared AetherClock time-frame types plus confidence-weighted cross-frame bit voting over a sliding window. Map-agnostic pure DSP/logic — no Qt, no GUI, no vendor ties. | unconverted |
148148
| `core/TxKeyingMarker.h` | 6 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted |

docs/automation-bridge.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3315,7 +3315,7 @@ Actions:
33153315

33163316
| action | value | effect |
33173317
|---|---|---|
3318-
| `snapshot` | optional pan target | Read `live`, current center/bandwidth MHz, waterfall/DSS history row counts, visible DSS row count, the current front-row peak bin/min/max/span, localized plateau metrics (`dssVisibleFrontMinValueBins`, `dssVisibleFrontLongestFlatRunBins`, and visible maxima), and flat/non-flat visible-row counts. |
3318+
| `snapshot` | optional pan target | Read `live`, current center/bandwidth MHz, waterfall/DSS history row counts, visible DSS row count, the current front-row peak bin/min/max/span, localized plateau metrics (`dssVisibleFrontMinValueBins`, `dssVisibleFrontLongestFlatRunBins`, and visible maxima), flat/non-flat visible-row counts, and the waterfall time-marker state (`waterfallTimeMarkerSeconds`, `waterfallTimeMarkers`). |
33193319
| `reset` | `native` or `kiwi` | Clear the selected stream's current/history rows and make that stream active for subsequent injection. |
33203320
| `inject` | `<count> <firstPeakBin> <stepBin> [native\|kiwi [rowLowMhz rowHighMhz]]` | Add synthetic rows with one strong peak per row. `count` is rejected if it exceeds the retained waterfall history capacity. Native injection adds one fallback-style waterfall/DSS row per input row; Kiwi injection drives `updateKiwiSdrWaterfallRow()`. Kiwi frame arguments override the source row's frequency span, so tests can cover partial-overlap rows. |
33213321
| `scrollback` | `<offsetRows>` | Enter waterfall history mode and rebuild the 3D surface using the same offset. |
@@ -3329,6 +3329,32 @@ injected peak becomes visible. The total row counts are still returned, but the
33293329
`*RowsAdded` fields are the deterministic assertion surface if live data is also
33303330
arriving between bridge requests.
33313331

3332+
### Waterfall time markers
3333+
3334+
`dss snapshot` reports the clock-aligned waterfall time markers (#5537):
3335+
3336+
| field | meaning |
3337+
|---|---|
3338+
| `waterfallTimeMarkerSeconds` | Selected interval for this pan slot, in seconds. `0` means Off (the default). Only `0`, `15`, `30`, `60`, `300`, `600` and `900` are valid; anything else fails closed to `0`. |
3339+
| `waterfallTimeMarkers` | Markers currently inside the waterfall viewport, newest first. Each entry is `{"timestampMs", "y"}`: `timestampMs` is the **clock boundary** the marker labels (always an exact multiple of the interval, never the packet arrival time), and `y` is its offset in pixels from the top of the waterfall rect. |
3340+
3341+
The interval is set from the panadapter context menu (**Waterfall Time
3342+
Markers**) and persists per pan slot in the `Display` settings document. It is
3343+
not settable over the bridge; seed `DisplaySettings` or use the menu.
3344+
3345+
Markers are attached to the signal row that was captured when the boundary was
3346+
crossed, so they scroll with the waterfall rather than with wall-clock time.
3347+
Two useful assertions:
3348+
3349+
- Every `timestampMs` is divisible by `waterfallTimeMarkerSeconds * 1000`.
3350+
- A marker's `y` advances at `1000 / waterfallTimeScaleMsPerRow` pixels per
3351+
second while live, and holds still under `dss scrollback`.
3352+
3353+
An empty array is normal: a screenful of waterfall is only
3354+
`waterfallRows * waterfallTimeScaleMsPerRow` milliseconds deep (typically
3355+
11-19 s), so intervals longer than that window have no marker on screen most
3356+
of the time.
3357+
33323358
To reproduce a low-coverage Kiwi row, read `centerMhz` and `bandwidthMhz` from
33333359
`dss snapshot`, then inject a Kiwi source row whose span overlaps less than 5%
33343360
of the current view. For example, with `viewHigh = centerMhz + bandwidthMhz/2`,

docs/theming/canonical-tokens.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,12 @@ PSK Reporter dark-map luminance ramp. Both bundled themes use the same dark
206206
cartographic palette; selecting the light app theme does not turn an explicitly
207207
enabled dark map light. These tokens affect only basemap images, not data
208208
overlays, attribution, or window chrome.
209+
210+
### Waterfall time markers
211+
212+
`color.waterfall.timeMarker.foreground` is the pale gray-blue foreground for
213+
UTC row annotations. Lines use 55% of its opacity; timestamp text uses its full
214+
opacity. `color.waterfall.timeMarker.background` provides a compact translucent
215+
dark text backing. Both bundled themes retain the same colors because waterfall
216+
signal palettes do not invert when application chrome changes theme. These
217+
annotations are independent of RX/TX, warning, and selection colors.

resources/themes/default-dark.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"accent.warning": "{color.amber.500}",
4949
"accent.danger": "{color.red.500}",
5050
"accent.success": "{color.green.500}",
51+
"waterfall.timeMarker.foreground": "#c8d8e8",
52+
"waterfall.timeMarker.background": "#dc0f0f1a",
5153
"waterfall.live": "{color.red.500}",
5254
"waterfall.history": "{color.gray.500}",
5355
"tx.mox.border": "#d08020",

resources/themes/default-light.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"accent.warning": "{color.amber.500}",
5050
"accent.danger": "{color.red.500}",
5151
"accent.success": "{color.green.500}",
52+
"waterfall.timeMarker.foreground": "#c8d8e8",
53+
"waterfall.timeMarker.background": "#dc0f0f1a",
5254
"waterfall.live": "{color.red.500}",
5355
"waterfall.history": "{color.gray.300}",
5456
"tx.mox.border": "#d08020",

src/core/ThemeSeedGenerated.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ void ThemeManager::seedGeneratedDefaults()
207207
}
208208
m_tokens.insert("color.waterfall.history", QString("#506070"));
209209
m_tokens.insert("color.waterfall.live", QString("#ff4d4d"));
210+
m_tokens.insert("color.waterfall.timeMarker.background", QString("#dc0f0f1a"));
211+
m_tokens.insert("color.waterfall.timeMarker.foreground", QString("#c8d8e8"));
210212
{
211213
ThemeFont f;
212214
f.family = QStringLiteral("DSEG7 Modern");

src/gui/DisplaySettings.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include "core/AppSettings.h"
4+
#include "WaterfallTimeMarkers.h"
45

56
#include <QJsonDocument>
67
#include <QJsonObject>
@@ -29,6 +30,28 @@ namespace AetherSDR {
2930
// spelling migrated by the now-removed TitleBarSettings helper.
3031
class DisplaySettings {
3132
public:
33+
static int waterfallTimeMarkerSeconds(int slot)
34+
{
35+
if (!isValidPanSlotIndex(slot)) {
36+
return 0;
37+
}
38+
return validWaterfallMarkerInterval(readObj()
39+
.value("waterfallTimeMarkers").toObject()
40+
.value(QString::number(slot)).toInt(0));
41+
}
42+
43+
static void setWaterfallTimeMarkerSeconds(int slot, int seconds)
44+
{
45+
if (!isValidPanSlotIndex(slot)) {
46+
return;
47+
}
48+
QJsonObject document = readObj();
49+
QJsonObject slotStates = document.value("waterfallTimeMarkers").toObject();
50+
slotStates[QString::number(slot)] = validWaterfallMarkerInterval(seconds);
51+
document["waterfallTimeMarkers"] = slotStates;
52+
write(document);
53+
}
54+
3255
// Global panadapter marker overlay preference. Default False preserves the
3356
// waterfall as signal history unless the operator opts into the overlay.
3457
static bool extendedPassband()

src/gui/SpectrumWidget.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include <QWheelEvent>
3939
#include <QNativeGestureEvent>
4040
#include <QMenu>
41+
#include <QActionGroup>
4142
#include <QToolTip>
4243
#include <QDialog>
4344
#include <QFormLayout>
@@ -1285,6 +1286,14 @@ QVariantMap SpectrumWidget::automationDssSnapshot() const
12851286
static_cast<qulonglong>(m_frequencyRangeCommandCount);
12861287
m[QStringLiteral("historyOffsetRows")] = m_wfHistoryOffsetRows;
12871288
m[QStringLiteral("maxHistoryOffsetRows")] = maxWaterfallHistoryOffsetRows();
1289+
m[QStringLiteral("waterfallTimeMarkerSeconds")] = m_wfTimeMarkerSeconds;
1290+
QVariantList timeMarkers;
1291+
for (const WaterfallTimeMarker& marker : visibleWaterfallTimeMarkers(m_waterfall.height())) {
1292+
timeMarkers.append(QVariantMap{
1293+
{QStringLiteral("timestampMs"), marker.timestampMs},
1294+
{QStringLiteral("y"), marker.y}});
1295+
}
1296+
m[QStringLiteral("waterfallTimeMarkers")] = timeMarkers;
12881297
m[QStringLiteral("waterfallRows")] = m_waterfall.height();
12891298
m[QStringLiteral("waterfallWidth")] = m_waterfall.width();
12901299
m[QStringLiteral("waterfallWriteRow")] = m_wfWriteRow;
@@ -2315,6 +2324,7 @@ SpectrumWidget::SpectrumWidget(QWidget* parent)
23152324
// recolours as it is scrolled back in.
23162325
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
23172326
this, [this]() {
2327+
m_wfTimeMarkerAtlasDirty = true;
23182328
rebuildWfStopsCacheFromTheme();
23192329
recolorWaterfallViewport();
23202330
markOverlayDirty();
@@ -2334,6 +2344,8 @@ SpectrumWidget::SpectrumWidget(QWidget* parent)
23342344
"color.background.2",
23352345
"color.background.3",
23362346
"color.background.spectrum",
2347+
"color.waterfall.timeMarker.foreground",
2348+
"color.waterfall.timeMarker.background",
23372349
"color.spectrum.trace",
23382350
"color.spectrum.peakHold",
23392351
"color.spectrum.average",
@@ -2490,6 +2502,8 @@ QString SpectrumWidget::settingsKey(const QString& base) const
24902502
void SpectrumWidget::setPanIndex(int idx)
24912503
{
24922504
m_panIndex = idx;
2505+
m_wfTimeMarkerSeconds = DisplaySettings::waterfallTimeMarkerSeconds(idx);
2506+
update();
24932507
// Let the overlay menu (the +RX/+TNF/Band/ANT/Display/Memory/DAX button
24942508
// rail) key its persisted collapsed/expanded state to this slot.
24952509
if (m_overlayMenu) {
@@ -2499,6 +2513,7 @@ void SpectrumWidget::setPanIndex(int idx)
24992513

25002514
void SpectrumWidget::loadSettings()
25012515
{
2516+
m_wfTimeMarkerSeconds = DisplaySettings::waterfallTimeMarkerSeconds(m_panIndex);
25022517
auto& s = AppSettings::instance();
25032518
// These four values are stored by the radio (including in profiles). Older
25042519
// releases persisted a competing client copy and reasserted it after status
@@ -5409,7 +5424,12 @@ void SpectrumWidget::appendVisibleRow(const QRgb* rowData,
54095424

54105425
QElapsedTimer timer;
54115426
timer.start();
5427+
if (m_wfVisibleTimeRows.size() != h) {
5428+
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(h);
5429+
}
5430+
const qint64 previousMs = m_wfVisibleTimeRows[m_wfWriteRow].timestampMs;
54125431
m_wfWriteRow = (m_wfWriteRow - 1 + h) % h;
5432+
m_wfVisibleTimeRows[m_wfWriteRow] = {m_wfIncomingTimestampMs, previousMs};
54135433
auto* row = reinterpret_cast<QRgb*>(m_waterfall.bits() + m_wfWriteRow * m_waterfall.bytesPerLine());
54145434
std::memcpy(row, rowData, m_waterfall.width() * sizeof(QRgb));
54155435
if (m_waterfallSupplemental.size() != m_waterfall.size()) {
@@ -5464,6 +5484,7 @@ void SpectrumWidget::appendHistoryRow(const quint8* intensityData,
54645484
double supplementalCenterMhz,
54655485
double supplementalBandwidthMhz)
54665486
{
5487+
m_wfIncomingTimestampMs = timestampMs;
54675488
// A hidden Flex/Kiwi source keeps only its small viewport and 96-row live
54685489
// DSS surface warm. Retained scrollback belongs to the visible source; it
54695490
// is rebuilt from new rows after a source switch (#4081, #4083).
@@ -5935,6 +5956,7 @@ void SpectrumWidget::paintWaterfallRowsFromHistory(
59355956
resetVisibleWaterfallFrequencyFrames(centerMhz, bandwidthMhz);
59365957
}
59375958

5959+
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(height);
59385960
const int w = m_waterfall.width();
59395961
const bool haveFrames = m_wfHistoryRowCenterMhz.size()
59405962
== m_waterfallHistory.capacityRows();
@@ -5965,6 +5987,10 @@ void SpectrumWidget::paintWaterfallRowsFromHistory(
59655987

59665988
const int destinationRow =
59675989
waterfallVisibleRowForAge(writeRowOrigin, age, height);
5990+
const int previousIndex = historyRowIndexForAge(m_wfHistoryOffsetRows + age + 1);
5991+
m_wfVisibleTimeRows[destinationRow] = {
5992+
m_wfHistoryTimestamps.value(rowIndex),
5993+
m_wfHistoryTimestamps.value(previousIndex)};
59685994
auto* dst = reinterpret_cast<QRgb*>(
59695995
m_waterfall.scanLine(destinationRow));
59705996
// With no stamped frames the rows carry no capture information at all,
@@ -6103,6 +6129,7 @@ void SpectrumWidget::rebuildWaterfallViewportForFrame(double centerMhz,
61036129
m_waterfallSupplemental.fill(Qt::black);
61046130
}
61056131
m_wfWriteRow = 0;
6132+
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(m_waterfall.height());
61066133
resetVisibleWaterfallFrequencyFrames(centerMhz, bandwidthMhz);
61076134
m_wfVisiblePaletteToken = waterfallPaletteToken();
61086135

@@ -6507,6 +6534,8 @@ void SpectrumWidget::clearDisplay()
65076534

65086535
void SpectrumWidget::clearCurrentWaterfallRows()
65096536
{
6537+
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(m_waterfall.height());
6538+
m_wfIncomingTimestampMs = 0;
65106539
if (!m_waterfall.isNull()) {
65116540
m_waterfall.fill(Qt::black);
65126541
}
@@ -6668,6 +6697,7 @@ void SpectrumWidget::saveCurrentWaterfallStreamState()
66686697
updated.waterfall = std::move(m_waterfall);
66696698
updated.waterfallSupplemental = std::move(m_waterfallSupplemental);
66706699
updated.wfWriteRow = m_wfWriteRow;
6700+
updated.visibleTimeRows = std::move(m_wfVisibleTimeRows);
66716701
updated.visibleRowCenterMhz = std::move(m_wfVisibleRowCenterMhz);
66726702
updated.visibleRowBwMhz = std::move(m_wfVisibleRowBwMhz);
66736703
updated.visibleSupplementalCenterMhz =
@@ -6790,6 +6820,10 @@ void SpectrumWidget::restoreCurrentWaterfallStreamState()
67906820
m_waterfallStreamSizeHint = m_waterfall.size();
67916821
}
67926822
m_wfWriteRow = restored.wfWriteRow;
6823+
// The viewport-size guard above preserves matching timestamp rows. Both
6824+
// marker readers also tolerate a mismatch: drawing skips it and append
6825+
// reinitializes the metadata before writing the next row.
6826+
m_wfVisibleTimeRows = std::move(restored.visibleTimeRows);
67936827
m_wfVisibleRowCenterMhz = std::move(restored.visibleRowCenterMhz);
67946828
m_wfVisibleRowBwMhz = std::move(restored.visibleRowBwMhz);
67956829
m_wfVisibleSupplementalCenterMhz =
@@ -10123,6 +10157,24 @@ void SpectrumWidget::mousePressEvent(QMouseEvent* ev)
1012310157
}
1012410158

1012510159
menu.addSeparator();
10160+
QMenu* timeMenu = menu.addMenu(tr("Waterfall Time Markers"));
10161+
timeMenu->setObjectName(QStringLiteral("waterfallTimeMarkersMenu"));
10162+
QActionGroup* timeGroup = new QActionGroup(timeMenu);
10163+
timeGroup->setExclusive(true);
10164+
for (const int seconds : kWaterfallMarkerIntervals) {
10165+
const QString label = seconds == 0 ? tr("Off")
10166+
: seconds < 60 ? tr("%1 seconds").arg(seconds)
10167+
: seconds == 60 ? tr("1 minute")
10168+
: tr("%1 minutes").arg(seconds / 60);
10169+
QAction* action = timeMenu->addAction(label);
10170+
action->setObjectName(QStringLiteral("waterfallTimeMarkers%1").arg(seconds));
10171+
action->setCheckable(true);
10172+
action->setChecked(seconds == m_wfTimeMarkerSeconds);
10173+
timeGroup->addAction(action);
10174+
connect(action, &QAction::triggered, this, [this, seconds]() {
10175+
setWaterfallTimeMarkerSeconds(seconds);
10176+
});
10177+
}
1012610178
QAction* tuneGuideAction = menu.addAction("Show Tune Guides");
1012710179
tuneGuideAction->setCheckable(true);
1012810180
tuneGuideAction->setChecked(m_showTuneGuides);
@@ -14805,6 +14857,10 @@ void SpectrumWidget::renderGpuFrame(QRhiCommandBuffer* cb,
1480514857
static_cast<quint64>(panStatsFftTimer.nsecsElapsed() / 1000);
1480614858
}
1480714859

14860+
prepareWaterfallTimeMarkersGpu(batch,
14861+
QRect(wfRect.x(), wfRect.y(),
14862+
std::min(wfContentW, waterfallTimeScaleRect(wfRect).left() - wfRect.left()),
14863+
wfRect.height()), logicalSize);
1480814864
cb->resourceUpdate(batch);
1480914865

1481014866
// Begin render pass
@@ -14987,6 +15043,7 @@ void SpectrumWidget::renderGpuFrame(QRhiCommandBuffer* cb,
1498715043
cb->draw(4);
1498815044
}
1498915045

15046+
drawWaterfallTimeMarkersGpu(cb);
1499015047
cb->endPass();
1499115048

1499215049
// VFO flag/widget repositioning now runs earlier (repositionVfoFlags(),
@@ -15041,6 +15098,7 @@ void SpectrumWidget::render(QRhiCommandBuffer* cb)
1504115098

1504215099
void SpectrumWidget::releaseResources()
1504315100
{
15101+
releaseWaterfallTimeMarkersGpu();
1504415102
releaseWaterfallFramePipelineResources();
1504515103
delete m_wfPipeline; m_wfPipeline = nullptr;
1504615104
delete m_wfSrb; m_wfSrb = nullptr;
@@ -15412,6 +15470,9 @@ void SpectrumWidget::paintEvent(QPaintEvent* ev)
1541215470
} else {
1541315471
drawDbmScale(p, specRect);
1541415472
}
15473+
drawWaterfallTimeMarkers(p, QRect(wfRect.x(), wfRect.y(),
15474+
std::min(wfContentRect.width(), waterfallTimeScaleRect(wfRect).left() - wfRect.left()),
15475+
wfRect.height()));
1541515476
drawTimeScale(p, wfRect);
1541615477

1541715478
if (PerfTelemetry::instance().enabled()) {

0 commit comments

Comments
 (0)