diff --git a/docs/architecture/aetherd-touchpoints.md b/docs/architecture/aetherd-touchpoints.md index 28a4a4be33..195a2ac5ac 100644 --- a/docs/architecture/aetherd-touchpoints.md +++ b/docs/architecture/aetherd-touchpoints.md @@ -160,7 +160,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine | `core/TailnetShimTokenStore.h` | 1 | peripheral(tailnet-shim) — OS-keychain storage of the tailnet shim's admin token, keyed by the Flex chassis serial (RFC #6271). Not SmartSDR wire: AetherSDR's own HTTP API to a separate service (the shim) that happens to run in the radio's container runtime, so a direct-transport peripheral, NOT behind the IRadioBackend radio seam (same shape as core/PgxlConnection.h). | 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` | 3 | 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` | 172 | 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` | 173 | 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` | 15 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted | diff --git a/docs/style/aethersdr-style-guide.md b/docs/style/aethersdr-style-guide.md index b910829c33..fa8f1c43fa 100644 --- a/docs/style/aethersdr-style-guide.md +++ b/docs/style/aethersdr-style-guide.md @@ -112,7 +112,7 @@ windows" lists what it keeps (the `FramelessMoveHelper` drag, Escape and Which windows the canon covers: short-lived, informational windows (About, Remote Access, and the candidates listed on #6239, once the maintainer picks them), and the tool windows the maintainer has moved to it (Waveforms, -Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio). Other tool and workspace dialogs keep +Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio, AGC-T Calibration). Other tool and workspace dialogs keep `PersistentDialog` and `FramelessWindowTitleBar` as `docs/agents/gui.md` describes. For windows the canon covers, `FramelessWindowTitleBar` is deprecated: those dialogs move to diff --git a/docs/style/dialog-patterns.md b/docs/style/dialog-patterns.md index c392c9553e..0b83381e58 100644 --- a/docs/style/dialog-patterns.md +++ b/docs/style/dialog-patterns.md @@ -236,7 +236,7 @@ replace these with a single `QSet>` walk. A window built on `CanonWindow` (`src/gui/CanonWindow.{h,cpp}`, RFC #6226) deliberately opts out of frameless chrome integration, and by default of geometry persistence too. About AetherSDR, Remote Access (Tailscale), -Waveforms, Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio (`ConnectionPanel`) and the AetherRX / AetherTX Settings (profile library) dialogs are built on it. +Waveforms, Network Diagnostics, AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to Radio (`ConnectionPanel`), AGC-T Calibration and the AetherRX / AetherTX Settings (profile library) dialogs are built on it. | Concern | `CanonWindow` | Why | |---|---|---| diff --git a/docs/style/theme-style-guide.md b/docs/style/theme-style-guide.md index e6d7c90e06..e412c8dec6 100644 --- a/docs/style/theme-style-guide.md +++ b/docs/style/theme-style-guide.md @@ -42,9 +42,9 @@ a widget subtree (for example, the `applet/tx` scope turns the checked-toggle accent red where the root theme has it blue — scope paths are slash-joined, and a widget opts in by carrying the path in its `themeContainer` property). The `canon` scope does the same for -whole windows: AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS and Connect to -Radio sit under `canon/aetherRx`, `canon/aetherTx`, `canon/aetherModem`, -`canon/aetherMap`, `canon/aetherGps` and `canon/connection`, and it maps the base tokens onto the canon palette +whole windows: AetherRX, AetherTX, AetherModem, AetherMap, AetherGPS, Connect to +Radio and AGC-T Calibration sit under `canon/aetherRx`, `canon/aetherTx`, `canon/aetherModem`, +`canon/aetherMap`, `canon/aetherGps`, `canon/connection` and `canon/agcCalibration`, and it maps the base tokens onto the canon palette there (see [`aethersdr-style-guide.md`](aethersdr-style-guide.md)). The widget-aware accessors below resolve scope automatically — prefer them inside applets and anything painted inside a scoped window. diff --git a/docs/user/docs/rx-controls.md b/docs/user/docs/rx-controls.md index 4cad13468f..28a2db51e8 100644 --- a/docs/user/docs/rx-controls.md +++ b/docs/user/docs/rx-controls.md @@ -118,6 +118,8 @@ The **SQL** button cycles through three states: **AGC-T calibration:** right-click the AGC-T slider, or use **Tools → Calibrate AGC-T...**, to calibrate the threshold against the measured noise floor on the active slice. It does not transmit. +Close the window with the corner ✕, Esc or Ctrl+W; closing it during +an Auto Sweep stops the sweep and puts back the AGC-T it started from. On the Hermes-Lite 2, AGC runs on the computer: with AGC **Off**, the AGC-T slider sets a fixed gain (4–64 dB, default 10 dB) remembered per receiver, and diff --git a/src/gui/AgcCalibrationDialog.cpp b/src/gui/AgcCalibrationDialog.cpp index a243934355..2ca6ab7390 100644 --- a/src/gui/AgcCalibrationDialog.cpp +++ b/src/gui/AgcCalibrationDialog.cpp @@ -1,6 +1,8 @@ #include "gui/AgcCalibrationDialog.h" +#include "gui/CanonIndicators.h" #include "core/AudioEngine.h" +#include "core/ThemeManager.h" #include "models/MeterModel.h" #include "models/RadioModel.h" #include "models/SliceModel.h" @@ -21,6 +23,66 @@ namespace AetherSDR { +namespace { + +// The style guide's canon vocabulary (RFC #6226), as the RX and TX Settings +// dialogs and AetherGPS use it: transparent over CanonWindow's ground, the +// mode line in ink, the hint muted, buttons and the target field on control. +constexpr const char* kAgcCalibrationStyle = R"( +QWidget { + color: {{color.canon.inkSoft}}; + background: transparent; +} +QLabel { + background: transparent; +} +QLabel[agcRole='mode'] { + color: {{color.canon.ink}}; + font-weight: 600; +} +QLabel[agcRole='result'] { + color: {{color.canon.cyan}}; + font-weight: 600; +} +QLabel[agcRole='hint'] { + color: {{color.canon.muted}}; +} +QPushButton { + color: {{color.canon.cyan}}; + background: {{color.canon.control}}; + border: 1px solid {{color.canon.lineHi}}; + border-radius: 4px; + padding: 5px 12px; + font-weight: 600; +} +QPushButton:hover { + background: {{color.canon.nested}}; + color: {{color.canon.aqua}}; +} +QPushButton:focus { + border-color: {{color.canon.aqua}}; +} +QPushButton:disabled { + color: {{color.canon.muted}}; + border-color: {{color.canon.line}}; + background: transparent; +} +QAbstractSpinBox { + color: {{color.canon.ink}}; + background: {{color.canon.control}}; + border: 1px solid {{color.canon.lineHi}}; + border-radius: 4px; + padding: 3px 6px; + selection-background-color: {{color.canon.cyan}}; + selection-color: {{color.canon.onAccent}}; +} +QAbstractSpinBox:focus { + border-color: {{color.canon.aqua}}; +} +)"; + +} // namespace + // =========================================================================== // AgcCurveWidget // =========================================================================== @@ -29,7 +91,8 @@ AgcCurveWidget::AgcCurveWidget(QWidget* parent) : QWidget(parent) { setMinimumSize(360, 200); - setAttribute(Qt::WA_StyledBackground, true); + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, + this, qOverload<>(&QWidget::update)); } void AgcCurveWidget::setCurve(const QVector& pts) @@ -80,13 +143,21 @@ void AgcCurveWidget::paintEvent(QPaintEvent*) QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); + const ThemeManager& tm = ThemeManager::instance(); + const auto canon = [&](const char* name) { + return tm.color(this, QStringLiteral("color.canon.") + QLatin1String(name)); + }; + const QRectF full = rect(); const qreal padL = 8.0, padR = 8.0, padT = 8.0, padB = 18.0; const QRectF plot(full.left() + padL, full.top() + padT, full.width() - padL - padR, full.height() - padT - padB); - p.fillRect(full, QColor(18, 20, 24)); - p.setPen(QPen(QColor(70, 76, 86), 1.0)); + p.setPen(QPen(canon("line"), 1.0)); + p.setBrush(canon("nested")); + p.drawRoundedRect(full.adjusted(0.5, 0.5, -0.5, -0.5), 7.0, 7.0); + p.setPen(QPen(canon("grid"), 1.0)); + p.setBrush(Qt::NoBrush); p.drawRect(plot); auto xForValue = [&](int v) { @@ -113,7 +184,7 @@ void AgcCurveWidget::paintEvent(QPaintEvent*) // AGC-off target line. if (m_strategy == AgcTCalibrator::Strategy::TargetLevel && m_targetDb >= yMin && m_targetDb <= yMax) { - p.setPen(QPen(QColor(90, 150, 220), 1.0, Qt::DashLine)); + p.setPen(QPen(canon("aqua"), 1.0, Qt::DashLine)); const qreal y = yForDb(m_targetDb); p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y)); } @@ -125,11 +196,11 @@ void AgcCurveWidget::paintEvent(QPaintEvent*) for (int i = 1; i < m_pts.size(); ++i) { path.lineTo(xForValue(m_pts[i].value), yForDb(m_pts[i].rmsDb)); } - p.setPen(QPen(QColor(120, 210, 140), 2.0)); + p.setPen(QPen(canon("cyan"), 2.0)); p.drawPath(path); } p.setPen(Qt::NoPen); - p.setBrush(QColor(150, 230, 170)); + p.setBrush(canon("aqua")); for (const auto& pt : m_pts) { p.drawEllipse(QPointF(xForValue(pt.value), yForDb(pt.rmsDb)), 2.0, 2.0); } @@ -137,9 +208,10 @@ void AgcCurveWidget::paintEvent(QPaintEvent*) // Recommended / knee marker. if (m_recommended >= 0) { const qreal x = xForValue(m_recommended); - p.setPen(QPen(QColor(245, 200, 90), 2.0)); + const QColor knee = canon("sparkGold"); + p.setPen(QPen(knee, 2.0)); p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom())); - p.setPen(QColor(245, 200, 90)); + p.setPen(knee); p.drawText(QPointF(std::min(x + 4, plot.right() - 70), plot.top() + 12), m_recIsKnee ? QStringLiteral("knee %1").arg(m_recommended) : QStringLiteral("target %1").arg(m_recommended)); @@ -148,12 +220,12 @@ void AgcCurveWidget::paintEvent(QPaintEvent*) // Current value marker. if (m_current >= 0) { const qreal x = xForValue(m_current); - p.setPen(QPen(QColor(220, 220, 230), 1.0, Qt::DotLine)); + p.setPen(QPen(canon("ink"), 1.0, Qt::DotLine)); p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom())); } // X axis labels. - p.setPen(QColor(150, 156, 166)); + p.setPen(canon("muted")); p.drawText(QPointF(plot.left(), plot.bottom() + 14), QStringLiteral("0")); p.drawText(QPointF(plot.right() - 24, plot.bottom() + 14), QStringLiteral("100")); p.drawText(QPointF(plot.center().x() - 30, plot.bottom() + 14), @@ -168,19 +240,28 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio, AudioEngine* audio, NoiseFloorFn noiseFloorFn, QWidget* parent) - : PersistentDialog(QStringLiteral("AGC-T Calibration"), - QStringLiteral("AgcCalibrationDialogGeometry"), parent), + : CanonWindow(QStringLiteral("AGC-T Calibration"), parent), m_radio(radio), m_audio(audio), m_noiseFloorFn(std::move(noiseFloorFn)) { - setMinimumSize(420, 360); - resize(460, 420); + setObjectName(QStringLiteral("agcCalibrationDialog")); + theme::setContainer(this, QStringLiteral("canon/agcCalibration")); + // The canon header adds about 40 px to the old 460x420. + setMinimumSize(420, 400); + resize(460, 460); + applyCanonSheet(bodyWidget(), [] { + return QString::fromLatin1(kAgcCalibrationStyle); + }); auto* body = new QVBoxLayout(bodyWidget()); - body->setContentsMargins(10, 10, 10, 10); + body->setContentsMargins(14, 12, 14, 14); body->setSpacing(8); + body->addWidget(makeCanonHeader(QStringLiteral("AGC-T Calibration"))); m_modeLabel = new QLabel(this); + m_modeLabel->setProperty("agcRole", QStringLiteral("mode")); + m_modeLabel->setWordWrap(true); m_quietLabel = new QLabel(this); + m_quietLabel->setWordWrap(true); body->addWidget(m_modeLabel); body->addWidget(m_quietLabel); @@ -188,6 +269,7 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio, body->addWidget(m_curve, 1); m_recLabel = new QLabel(this); + m_recLabel->setProperty("agcRole", QStringLiteral("result")); body->addWidget(m_recLabel); // AGC-off advanced: comfortable-noise target. @@ -219,6 +301,7 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio, m_hintLabel = new QLabel( QStringLiteral("Tune to a clear spot, then Auto Sweep — or move the AGC-T " "slider and watch where the noise bends."), this); + m_hintLabel->setProperty("agcRole", QStringLiteral("hint")); m_hintLabel->setWordWrap(true); body->addWidget(m_hintLabel); @@ -252,6 +335,22 @@ AgcCalibrationDialog::AgcCalibrationDialog(RadioModel* radio, updateModeUi(); } +AgcCalibrationDialog::~AgcCalibrationDialog() +{ + // Destroyed without a close (the app quitting): put the AGC-T back too. + if (m_engine.isRunning()) { + m_engine.stop(); + } +} + +void AgcCalibrationDialog::done(int result) +{ + if (m_engine.isRunning()) { + m_engine.stop(); // restores the original value + } + CanonWindow::done(result); +} + void AgcCalibrationDialog::setSlice(SliceModel* slice) { if (m_slice == slice) { diff --git a/src/gui/AgcCalibrationDialog.h b/src/gui/AgcCalibrationDialog.h index 6ce83e0439..bca72412cf 100644 --- a/src/gui/AgcCalibrationDialog.h +++ b/src/gui/AgcCalibrationDialog.h @@ -1,6 +1,6 @@ #pragma once -#include "gui/PersistentDialog.h" +#include "gui/CanonWindow.h" #include "core/AgcTCalibrator.h" #include @@ -23,10 +23,7 @@ class AudioEngine; // AgcCurveWidget — lightweight 2D plot of post-AGC audio noise (y, dB) versus // the AGC-T value (x, 0..100). Markers: current value, detected knee / // recommended value, and (AGC-off) the comfortable-noise target line. -// -// Visual styling here is deliberately plain — final visual design is the -// maintainer's call (CLAUDE.md). This draws the data correctly; colors/fonts -// are placeholders. +// Painted in the canon palette (color.canon.*), so it follows the theme. class AgcCurveWidget : public QWidget { Q_OBJECT public: @@ -51,11 +48,11 @@ class AgcCurveWidget : public QWidget { AgcTCalibrator::Strategy m_strategy{AgcTCalibrator::Strategy::Knee}; }; -// AgcCalibrationDialog — the calibration surface. Auto-sweep first (one click), -// with the live curve always visible so the operator can fine-tune by hand. -// Operates on the active slice; the radio stays authoritative (we only set the -// value through SliceModel). -class AgcCalibrationDialog : public PersistentDialog { +// AgcCalibrationDialog — the calibration surface, a canon dialog. Auto-sweep +// first (one click), with the live curve always visible so the operator can +// fine-tune by hand. Operates on the active slice; the radio stays +// authoritative (we only set the value through SliceModel). +class AgcCalibrationDialog : public CanonWindow { Q_OBJECT public: using NoiseFloorFn = std::function; @@ -64,6 +61,11 @@ class AgcCalibrationDialog : public PersistentDialog { AudioEngine* audio, NoiseFloorFn noiseFloorFn, QWidget* parent = nullptr); + ~AgcCalibrationDialog() override; + + // Every close path (the close button, Escape, Ctrl+W) ends here: a running + // sweep stops and the AGC-T it found goes back, as Stop does. + void done(int result) override; // Point the panel at a slice (called on show and on active-slice change). void setSlice(SliceModel* slice); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 898292943c..f61d1a2cdf 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -4733,10 +4733,17 @@ void MainWindow::showAgcCalibrationDialog(int sliceId) SpectrumWidget* sw = s ? spectrumForSlice(s) : nullptr; return sw ? sw->noiseFloorDbm() : std::numeric_limits::quiet_NaN(); }; - showOrRaisePersistent(m_agcCalibrationDialog, &m_radioModel, m_audio, floorFn); - if (m_agcCalibrationDialog) { - m_agcCalibrationDialog->setSlice(slice); + // A canon dialog, so not showOrRaisePersistent(): that applies View → + // Frameless Window, which a CanonWindow does not follow. + if (!m_agcCalibrationDialog) { + auto* dlg = new AgcCalibrationDialog(&m_radioModel, m_audio, floorFn, this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + m_agcCalibrationDialog = dlg; } + m_agcCalibrationDialog->setSlice(slice); + m_agcCalibrationDialog->show(); + m_agcCalibrationDialog->raise(); + m_agcCalibrationDialog->activateWindow(); } // AX.25 dialog + KISS TNC startup live in MainWindow_DigitalModes.cpp (#3351 Phase 1e).