Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ set(GUI_SOURCES
src/gui/PanadapterMessageOverlay.h
src/gui/PanadapterRenderScheduler.cpp
src/gui/SpectrumWidget.cpp
src/gui/SpectrumWidget_TimeMarkers.cpp
src/gui/WaterfallHistoryBuffer.cpp
src/gui/SpectrumOverlayMenu.cpp
src/gui/SpectrumOverlayWheelGuard.cpp
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/aetherd-touchpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
| `core/SystemInventory.h` | 1 | ui-support — Startup and support-bundle inventory of host CPU, SIMD and RAM capabilities. Process diagnostics, not radio state. | unconverted |
| `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 |
| `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 |
| `core/ThemeManager.h` | 145 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
| `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 |
| `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 |
| `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 |
| `core/TxKeyingMarker.h` | 6 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted |
Expand Down
9 changes: 9 additions & 0 deletions docs/theming/canonical-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,12 @@ PSK Reporter dark-map luminance ramp. Both bundled themes use the same dark
cartographic palette; selecting the light app theme does not turn an explicitly
enabled dark map light. These tokens affect only basemap images, not data
overlays, attribution, or window chrome.

### Waterfall time markers

`color.waterfall.timeMarker.foreground` is the pale gray-blue foreground for
UTC row annotations. Lines use 55% of its opacity; timestamp text uses its full
opacity. `color.waterfall.timeMarker.background` provides a compact translucent
dark text backing. Both bundled themes retain the same colors because waterfall
signal palettes do not invert when application chrome changes theme. These
annotations are independent of RX/TX, warning, and selection colors.
2 changes: 2 additions & 0 deletions resources/themes/default-dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"accent.warning": "{color.amber.500}",
"accent.danger": "{color.red.500}",
"accent.success": "{color.green.500}",
"waterfall.timeMarker.foreground": "#c8d8e8",
"waterfall.timeMarker.background": "#dc0f0f1a",
"waterfall.live": "{color.red.500}",
"waterfall.history": "{color.gray.500}",
"tx.mox.border": "#d08020",
Expand Down
2 changes: 2 additions & 0 deletions resources/themes/default-light.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"accent.warning": "{color.amber.500}",
"accent.danger": "{color.red.500}",
"accent.success": "{color.green.500}",
"waterfall.timeMarker.foreground": "#c8d8e8",
"waterfall.timeMarker.background": "#dc0f0f1a",
"waterfall.live": "{color.red.500}",
"waterfall.history": "{color.gray.300}",
"tx.mox.border": "#d08020",
Expand Down
2 changes: 2 additions & 0 deletions src/core/ThemeSeedGenerated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ void ThemeManager::seedGeneratedDefaults()
}
m_tokens.insert("color.waterfall.history", QString("#506070"));
m_tokens.insert("color.waterfall.live", QString("#ff4d4d"));
m_tokens.insert("color.waterfall.timeMarker.background", QString("#dc0f0f1a"));
m_tokens.insert("color.waterfall.timeMarker.foreground", QString("#c8d8e8"));
{
ThemeFont f;
f.family = QStringLiteral("DSEG7 Modern");
Expand Down
23 changes: 23 additions & 0 deletions src/gui/DisplaySettings.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "core/AppSettings.h"
#include "WaterfallTimeMarkers.h"

#include <QJsonDocument>
#include <QJsonObject>
Expand Down Expand Up @@ -29,6 +30,28 @@ namespace AetherSDR {
// spelling migrated by the now-removed TitleBarSettings helper.
class DisplaySettings {
public:
static int waterfallTimeMarkerSeconds(int slot)
{
if (!isValidPanSlotIndex(slot)) {
return 0;
}
return validWaterfallMarkerInterval(readObj()
.value("waterfallTimeMarkers").toObject()
.value(QString::number(slot)).toInt(0));
}

static void setWaterfallTimeMarkerSeconds(int slot, int seconds)
{
if (!isValidPanSlotIndex(slot)) {
return;
}
QJsonObject document = readObj();
QJsonObject slotStates = document.value("waterfallTimeMarkers").toObject();
slotStates[QString::number(slot)] = validWaterfallMarkerInterval(seconds);
document["waterfallTimeMarkers"] = slotStates;
write(document);
}

// Global panadapter marker overlay preference. Default False preserves the
// waterfall as signal history unless the operator opts into the overlay.
static bool extendedPassband()
Expand Down
61 changes: 61 additions & 0 deletions src/gui/SpectrumWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include <QWheelEvent>
#include <QNativeGestureEvent>
#include <QMenu>
#include <QActionGroup>
#include <QToolTip>
#include <QDialog>
#include <QFormLayout>
Expand Down Expand Up @@ -1283,6 +1284,14 @@ QVariantMap SpectrumWidget::automationDssSnapshot() const
static_cast<qulonglong>(m_frequencyRangeCommandCount);
m[QStringLiteral("historyOffsetRows")] = m_wfHistoryOffsetRows;
m[QStringLiteral("maxHistoryOffsetRows")] = maxWaterfallHistoryOffsetRows();
m[QStringLiteral("waterfallTimeMarkerSeconds")] = m_wfTimeMarkerSeconds;
QVariantList timeMarkers;
for (const WaterfallTimeMarker& marker : visibleWaterfallTimeMarkers(m_waterfall.height())) {
timeMarkers.append(QVariantMap{
{QStringLiteral("timestampMs"), marker.timestampMs},
{QStringLiteral("y"), marker.y}});
}
m[QStringLiteral("waterfallTimeMarkers")] = timeMarkers;
m[QStringLiteral("waterfallRows")] = m_waterfall.height();
m[QStringLiteral("waterfallWidth")] = m_waterfall.width();
m[QStringLiteral("waterfallWriteRow")] = m_wfWriteRow;
Expand Down Expand Up @@ -2313,6 +2322,7 @@ SpectrumWidget::SpectrumWidget(QWidget* parent)
// recolours as it is scrolled back in.
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
this, [this]() {
m_wfTimeMarkerAtlasDirty = true;
rebuildWfStopsCacheFromTheme();
recolorWaterfallViewport();
markOverlayDirty();
Expand All @@ -2332,6 +2342,8 @@ SpectrumWidget::SpectrumWidget(QWidget* parent)
"color.background.2",
"color.background.3",
"color.background.spectrum",
"color.waterfall.timeMarker.foreground",
"color.waterfall.timeMarker.background",
"color.spectrum.trace",
"color.spectrum.peakHold",
"color.spectrum.average",
Expand Down Expand Up @@ -2488,6 +2500,8 @@ QString SpectrumWidget::settingsKey(const QString& base) const
void SpectrumWidget::setPanIndex(int idx)
{
m_panIndex = idx;
m_wfTimeMarkerSeconds = DisplaySettings::waterfallTimeMarkerSeconds(idx);
update();
// Let the overlay menu (the +RX/+TNF/Band/ANT/Display/Memory/DAX button
// rail) key its persisted collapsed/expanded state to this slot.
if (m_overlayMenu) {
Expand All @@ -2497,6 +2511,7 @@ void SpectrumWidget::setPanIndex(int idx)

void SpectrumWidget::loadSettings()
{
m_wfTimeMarkerSeconds = DisplaySettings::waterfallTimeMarkerSeconds(m_panIndex);
auto& s = AppSettings::instance();
// These four values are stored by the radio (including in profiles). Older
// releases persisted a competing client copy and reasserted it after status
Expand Down Expand Up @@ -5407,7 +5422,12 @@ void SpectrumWidget::appendVisibleRow(const QRgb* rowData,

QElapsedTimer timer;
timer.start();
if (m_wfVisibleTimeRows.size() != h) {
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(h);
}
const qint64 previousMs = m_wfVisibleTimeRows[m_wfWriteRow].timestampMs;
m_wfWriteRow = (m_wfWriteRow - 1 + h) % h;
m_wfVisibleTimeRows[m_wfWriteRow] = {m_wfIncomingTimestampMs, previousMs};
auto* row = reinterpret_cast<QRgb*>(m_waterfall.bits() + m_wfWriteRow * m_waterfall.bytesPerLine());
std::memcpy(row, rowData, m_waterfall.width() * sizeof(QRgb));
if (m_waterfallSupplemental.size() != m_waterfall.size()) {
Expand Down Expand Up @@ -5462,6 +5482,7 @@ void SpectrumWidget::appendHistoryRow(const quint8* intensityData,
double supplementalCenterMhz,
double supplementalBandwidthMhz)
{
m_wfIncomingTimestampMs = timestampMs;
// A hidden Flex/Kiwi source keeps only its small viewport and 96-row live
// DSS surface warm. Retained scrollback belongs to the visible source; it
// is rebuilt from new rows after a source switch (#4081, #4083).
Expand Down Expand Up @@ -5933,6 +5954,7 @@ void SpectrumWidget::paintWaterfallRowsFromHistory(
resetVisibleWaterfallFrequencyFrames(centerMhz, bandwidthMhz);
}

m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(height);
const int w = m_waterfall.width();
const bool haveFrames = m_wfHistoryRowCenterMhz.size()
== m_waterfallHistory.capacityRows();
Expand Down Expand Up @@ -5963,6 +5985,10 @@ void SpectrumWidget::paintWaterfallRowsFromHistory(

const int destinationRow =
waterfallVisibleRowForAge(writeRowOrigin, age, height);
const int previousIndex = historyRowIndexForAge(m_wfHistoryOffsetRows + age + 1);
m_wfVisibleTimeRows[destinationRow] = {
m_wfHistoryTimestamps.value(rowIndex),
m_wfHistoryTimestamps.value(previousIndex)};
auto* dst = reinterpret_cast<QRgb*>(
m_waterfall.scanLine(destinationRow));
// With no stamped frames the rows carry no capture information at all,
Expand Down Expand Up @@ -6101,6 +6127,7 @@ void SpectrumWidget::rebuildWaterfallViewportForFrame(double centerMhz,
m_waterfallSupplemental.fill(Qt::black);
}
m_wfWriteRow = 0;
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(m_waterfall.height());
resetVisibleWaterfallFrequencyFrames(centerMhz, bandwidthMhz);
m_wfVisiblePaletteToken = waterfallPaletteToken();

Expand Down Expand Up @@ -6505,6 +6532,8 @@ void SpectrumWidget::clearDisplay()

void SpectrumWidget::clearCurrentWaterfallRows()
{
m_wfVisibleTimeRows = QVector<WaterfallTimeRow>(m_waterfall.height());
m_wfIncomingTimestampMs = 0;
if (!m_waterfall.isNull()) {
m_waterfall.fill(Qt::black);
}
Expand Down Expand Up @@ -6666,6 +6695,7 @@ void SpectrumWidget::saveCurrentWaterfallStreamState()
updated.waterfall = std::move(m_waterfall);
updated.waterfallSupplemental = std::move(m_waterfallSupplemental);
updated.wfWriteRow = m_wfWriteRow;
updated.visibleTimeRows = std::move(m_wfVisibleTimeRows);
updated.visibleRowCenterMhz = std::move(m_wfVisibleRowCenterMhz);
updated.visibleRowBwMhz = std::move(m_wfVisibleRowBwMhz);
updated.visibleSupplementalCenterMhz =
Expand Down Expand Up @@ -6788,6 +6818,10 @@ void SpectrumWidget::restoreCurrentWaterfallStreamState()
m_waterfallStreamSizeHint = m_waterfall.size();
}
m_wfWriteRow = restored.wfWriteRow;
// The viewport-size guard above preserves matching timestamp rows. Both
// marker readers also tolerate a mismatch: drawing skips it and append
// reinitializes the metadata before writing the next row.
m_wfVisibleTimeRows = std::move(restored.visibleTimeRows);
Comment thread
rfoust marked this conversation as resolved.
m_wfVisibleRowCenterMhz = std::move(restored.visibleRowCenterMhz);
m_wfVisibleRowBwMhz = std::move(restored.visibleRowBwMhz);
m_wfVisibleSupplementalCenterMhz =
Expand Down Expand Up @@ -10113,6 +10147,24 @@ void SpectrumWidget::mousePressEvent(QMouseEvent* ev)
}

menu.addSeparator();
QMenu* timeMenu = menu.addMenu(tr("Waterfall Time Markers"));
timeMenu->setObjectName(QStringLiteral("waterfallTimeMarkersMenu"));
QActionGroup* timeGroup = new QActionGroup(timeMenu);
timeGroup->setExclusive(true);
for (const int seconds : kWaterfallMarkerIntervals) {
const QString label = seconds == 0 ? tr("Off")
: seconds < 60 ? tr("%1 seconds").arg(seconds)
: seconds < 3600 ? (seconds == 60 ? tr("1 minute") : tr("%1 minutes").arg(seconds / 60))
: tr("1 hour");
QAction* action = timeMenu->addAction(label);
action->setObjectName(QStringLiteral("waterfallTimeMarkers%1").arg(seconds));
action->setCheckable(true);
action->setChecked(seconds == m_wfTimeMarkerSeconds);
timeGroup->addAction(action);
connect(action, &QAction::triggered, this, [this, seconds]() {
setWaterfallTimeMarkerSeconds(seconds);
});
}
QAction* tuneGuideAction = menu.addAction("Show Tune Guides");
tuneGuideAction->setCheckable(true);
tuneGuideAction->setChecked(m_showTuneGuides);
Expand Down Expand Up @@ -14786,6 +14838,10 @@ void SpectrumWidget::renderGpuFrame(QRhiCommandBuffer* cb,
static_cast<quint64>(panStatsFftTimer.nsecsElapsed() / 1000);
}

prepareWaterfallTimeMarkersGpu(batch,
QRect(wfRect.x(), wfRect.y(),
std::min(wfContentW, waterfallTimeScaleRect(wfRect).left() - wfRect.left()),
wfRect.height()), logicalSize);
cb->resourceUpdate(batch);

// Begin render pass
Expand Down Expand Up @@ -14968,6 +15024,7 @@ void SpectrumWidget::renderGpuFrame(QRhiCommandBuffer* cb,
cb->draw(4);
}

drawWaterfallTimeMarkersGpu(cb);
cb->endPass();

// VFO flag/widget repositioning now runs earlier (repositionVfoFlags(),
Expand Down Expand Up @@ -15022,6 +15079,7 @@ void SpectrumWidget::render(QRhiCommandBuffer* cb)

void SpectrumWidget::releaseResources()
{
releaseWaterfallTimeMarkersGpu();
releaseWaterfallFramePipelineResources();
delete m_wfPipeline; m_wfPipeline = nullptr;
delete m_wfSrb; m_wfSrb = nullptr;
Expand Down Expand Up @@ -15393,6 +15451,9 @@ void SpectrumWidget::paintEvent(QPaintEvent* ev)
} else {
drawDbmScale(p, specRect);
}
drawWaterfallTimeMarkers(p, QRect(wfRect.x(), wfRect.y(),
std::min(wfContentRect.width(), waterfallTimeScaleRect(wfRect).left() - wfRect.left()),
wfRect.height()));
drawTimeScale(p, wfRect);

if (PerfTelemetry::instance().enabled()) {
Expand Down
27 changes: 27 additions & 0 deletions src/gui/SpectrumWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <QPointer>
#include <QMap>
#include <QImage>
#include <QFont>
#include <QColor>
#include <QDateTime>
#include <QElapsedTimer>
Expand All @@ -27,6 +28,7 @@
#include "DssRenderer.h"
#include "SpectrumPreviewLogic.h"
#include "WaterfallHistoryBuffer.h"
#include "WaterfallTimeMarkers.h"

class QVariantAnimation;
class QSoundEffect;
Expand Down Expand Up @@ -139,6 +141,10 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
// Per-pan settings persistence
void setPanIndex(int idx);
int panIndex() const { return m_panIndex; }
Q_PROPERTY(int waterfallTimeMarkerSeconds READ waterfallTimeMarkerSeconds WRITE setWaterfallTimeMarkerSeconds)
int waterfallTimeMarkerSeconds() const { return m_wfTimeMarkerSeconds; }
void setWaterfallTimeMarkerSeconds(int seconds);

QString settingsKey(const QString& base) const;
void loadSettings();

Expand Down Expand Up @@ -1091,6 +1097,7 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
QImage waterfall;
QImage waterfallSupplemental;
int wfWriteRow{0};
QVector<WaterfallTimeRow> visibleTimeRows;
QVector<double> visibleRowCenterMhz;
QVector<double> visibleRowBwMhz;
QVector<double> visibleSupplementalCenterMhz;
Expand Down Expand Up @@ -1217,6 +1224,14 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
const QRgb* supplementalRowData = nullptr,
double supplementalCenterMhz = -1.0,
double supplementalBandwidthMhz = -1.0);
QVector<WaterfallTimeMarker> visibleWaterfallTimeMarkers(qreal height) const;
void prepareWaterfallTimeMarkerAtlas(const QVector<WaterfallTimeMarker>& markers);
void drawWaterfallTimeMarkers(QPainter& painter, const QRect& rect);
#ifdef AETHER_GPU_SPECTRUM
void prepareWaterfallTimeMarkersGpu(QRhiResourceUpdateBatch* batch, const QRect& rect, const QSize& logicalSize);
void drawWaterfallTimeMarkersGpu(QRhiCommandBuffer* cb);
void releaseWaterfallTimeMarkersGpu();
#endif
int waterfallHistoryCapacityRows() const;
int maxWaterfallHistoryOffsetRows() const;
int historyRowIndexForAge(int ageRows) const;
Expand Down Expand Up @@ -1608,6 +1623,14 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
float m_wfMaxDbm{-50.0f};

// Scrolling waterfall image (Format_RGB32)
int m_wfTimeMarkerSeconds{0};
qint64 m_wfIncomingTimestampMs{0};
QVector<WaterfallTimeRow> m_wfVisibleTimeRows;
QImage m_wfTimeMarkerAtlas;
QFont m_wfTimeMarkerAtlasFont;
QVector<qint64> m_wfTimeMarkerLabels;
bool m_wfTimeMarkerAtlasDirty{true};
int m_wfTimeMarkerLabelHeight{0};
QImage m_waterfall;
// Same ring topology as m_waterfall. Native FLEX tiles are rasterized over
// their full (wider) frequency frame here; the primary viewport row wins
Expand Down Expand Up @@ -2023,6 +2046,10 @@ class SpectrumWidget : public SPECTRUM_BASE_CLASS {
bool m_kiwiSdrDisplaySourceKiwi{false};

#ifdef AETHER_GPU_SPECTRUM
QRhiTexture* m_wfTimeMarkerTexture{nullptr};
QRhiShaderResourceBindings* m_wfTimeMarkerSrb{nullptr};
QRhiBuffer* m_wfTimeMarkerVbo{nullptr};
int m_wfTimeMarkerQuadCount{0};
bool m_rhiInitialized{false};
bool m_rhiFailureForcedForAutomation{false};
SpectrumRhiFailureState m_rhiFailure;
Expand Down
Loading
Loading