From 2973ddb97449e37cbb94876a9bf994a60561db28 Mon Sep 17 00:00:00 2001 From: ea5wa Date: Fri, 5 Jun 2026 17:17:08 +0200 Subject: [PATCH 1/6] feat(gui): add PanLock button to TitleBar Adds a toggleable Pan Lock button to the title bar that keeps the panadapter centered on Slice A frequency. Useful for Doppler tracking via CAT (e.g. SatPC32). - TitleBar: new m_panFollowBtn widget + panFollowToggled signal - MainWindow: setPanFollow() slot wired to TitleBar::panFollowToggled --- src/gui/MainWindow.cpp | 31 +++++++++++++++++++++++++++++++ src/gui/MainWindow.h | 4 ++++ src/gui/TitleBar.cpp | 28 ++++++++++++++++++++++++++++ src/gui/TitleBar.h | 2 ++ 4 files changed, 65 insertions(+) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 5ff0c2636..90bc87a15 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3464,6 +3464,10 @@ MainWindow::MainWindow(QWidget* parent) // Overlay-menu antenna wiring is now per-pan in wirePanadapter() (#1260). // Antenna list and S-meter are now wired per-widget in onSliceAdded. + // ── Title bar: Pan Follow ──────────────────────────────────────────────── + connect(m_titleBar, &TitleBar::panFollowToggled, + this, &MainWindow::setPanFollow); + // ── Title bar: PC Audio, master volume, headphone volume ──────────────── // The remote_audio_rx stream controls the radio's audio routing: // stream exists → audio to PC; stream removed → audio to radio speakers. @@ -19126,4 +19130,31 @@ void MainWindow::onSpectrumReadyForSHistory(quint32 streamId, const QVectorpanId(); + if (panId.isEmpty()) return; + const double freq = s->frequency(); + auto* pan = m_radioModel.panadapter(panId); + if (pan && qFuzzyCompare(pan->centerMhz(), freq)) return; + const QString freqStr = QString::number(freq, 'f', 6); + if (pan) pan->applyPanStatus({{"center", freqStr}}); + m_radioModel.sendCommand( + QString("display pan set %1 center=%2").arg(panId, freqStr)); + }; + + centerPan(); + m_panFollowConn = connect(s, &SliceModel::frequencyChanged, + this, [centerPan](double) { centerPan(); }); +} + } // namespace AetherSDR diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index b69b01593..4c2d8f6ea 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -929,6 +929,10 @@ private slots: void onFdvMetersChanged(); #endif + // Pan Follow — keeps the panadapter centered on Slice A frequency + QMetaObject::Connection m_panFollowConn; + void setPanFollow(bool on); + #if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE) DaxBridge* m_daxBridge{nullptr}; QString m_savedMicSelection; // restore on stopDax diff --git a/src/gui/TitleBar.cpp b/src/gui/TitleBar.cpp index 61963c2ad..3db077c52 100644 --- a/src/gui/TitleBar.cpp +++ b/src/gui/TitleBar.cpp @@ -219,6 +219,34 @@ TitleBar::TitleBar(QWidget* parent) // ── PC Audio + Master Vol + HP Vol ────────────────────────────────────── auto& s = AppSettings::instance(); + // Pan Follow toggle — keeps the panadapter centered on Slice A frequency + m_panFollowBtn = new QPushButton("Pan Lock"); + m_panFollowBtn->setCheckable(true); + m_panFollowBtn->setChecked(false); + m_panFollowBtn->setFixedHeight(22); + m_panFollowBtn->setFixedWidth(70); + m_panFollowBtn->setAccessibleName("Pan follow slice"); + m_panFollowBtn->setAccessibleDescription("Keep panadapter centered on Slice A frequency"); + m_panFollowBtn->setToolTip("Pan Lock — keeps the panadapter centered on Slice A frequency (e.g. for Doppler tracking)"); + + auto updatePanFollowStyle = [this]() { + m_panFollowBtn->setStyleSheet(m_panFollowBtn->isChecked() + ? "QPushButton { background: #1e4a8a; color: #b0e8ff; border: 1px solid #4090d0; " + "border-radius: 3px; font-size: 10px; font-weight: bold; }" + "QPushButton:hover { background: #2558a0; }" + : "QPushButton { background: #1a2a3a; color: #607080; border: 1px solid #304050; " + "border-radius: 3px; font-size: 10px; font-weight: bold; }" + "QPushButton:hover { background: #243848; }"); + }; + updatePanFollowStyle(); + + connect(m_panFollowBtn, &QPushButton::toggled, this, [this, updatePanFollowStyle](bool on) { + updatePanFollowStyle(); + emit panFollowToggled(on); + }); + m_hbox->addWidget(m_panFollowBtn); + m_hbox->addSpacing(4); + // PC Audio toggle m_pcBtn = new QPushButton("PC Audio"); m_pcBtn->setCheckable(true); diff --git a/src/gui/TitleBar.h b/src/gui/TitleBar.h index afb2a9230..825ab38c2 100644 --- a/src/gui/TitleBar.h +++ b/src/gui/TitleBar.h @@ -52,6 +52,7 @@ class TitleBar : public QWidget { bool isSystemMoveAreaAt(const QPoint& globalPos) const; signals: + void panFollowToggled(bool on); void pcAudioToggled(bool on); void masterVolumeChanged(int pct); void headphoneVolumeChanged(int pct); @@ -87,6 +88,7 @@ class TitleBar : public QWidget { QLabel* m_appNameLabel{nullptr}; QLabel* m_otherTxLabel{nullptr}; QPushButton* m_mfBtn{nullptr}; + QPushButton* m_panFollowBtn{nullptr}; QPushButton* m_pcBtn{nullptr}; QPushButton* m_speakerBtn{nullptr}; QPushButton* m_headphoneBtn{nullptr}; From 1e270b3f25be6c3c9ffe38bd627de7adbe03561e Mon Sep 17 00:00:00 2001 From: ea5wa Date: Fri, 5 Jun 2026 17:29:45 +0200 Subject: [PATCH 2/6] chore: ignore local scratch txt file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ed4a6b8a..b58fdc7a6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ msix-root-*/ *.msixupload *.pfx packaging/windows/certs/ +Resumen para empezar de nuevo.txt From 4fe19669a6d444ab3505d1c2fe030fc8b0be0a93 Mon Sep 17 00:00:00 2001 From: ea5wa Date: Fri, 5 Jun 2026 23:05:56 +0200 Subject: [PATCH 3/6] fix(gui): persist PanLock state; move scratch file to global gitignore - TitleBar: read PanLockEnabled from AppSettings on init so button restores its last state on every launch - TitleBar: write PanLockEnabled to AppSettings on every toggle - .gitignore: remove personal scratch file entry (moved to ~/.gitignore_global per project conventions) --- .gitignore | 1 - src/gui/TitleBar.cpp | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b58fdc7a6..6ed4a6b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,3 @@ msix-root-*/ *.msixupload *.pfx packaging/windows/certs/ -Resumen para empezar de nuevo.txt diff --git a/src/gui/TitleBar.cpp b/src/gui/TitleBar.cpp index 3db077c52..ed10fd931 100644 --- a/src/gui/TitleBar.cpp +++ b/src/gui/TitleBar.cpp @@ -222,7 +222,8 @@ TitleBar::TitleBar(QWidget* parent) // Pan Follow toggle — keeps the panadapter centered on Slice A frequency m_panFollowBtn = new QPushButton("Pan Lock"); m_panFollowBtn->setCheckable(true); - m_panFollowBtn->setChecked(false); + bool panLockOn = s.value("PanLockEnabled", "False").toString() == "True"; + m_panFollowBtn->setChecked(panLockOn); m_panFollowBtn->setFixedHeight(22); m_panFollowBtn->setFixedWidth(70); m_panFollowBtn->setAccessibleName("Pan follow slice"); @@ -242,6 +243,9 @@ TitleBar::TitleBar(QWidget* parent) connect(m_panFollowBtn, &QPushButton::toggled, this, [this, updatePanFollowStyle](bool on) { updatePanFollowStyle(); + auto& ss = AppSettings::instance(); + ss.setValue("PanLockEnabled", on ? "True" : "False"); + ss.save(); emit panFollowToggled(on); }); m_hbox->addWidget(m_panFollowBtn); From 9dc9055a5fc5dbd620130a77b1fc1ff7910becbb Mon Sep 17 00:00:00 2001 From: ea5wa Date: Sat, 6 Jun 2026 22:32:09 +0200 Subject: [PATCH 4/6] fix(gui): engage PanLock on startup and re-attach on slice recreation - TitleBar: add isPanFollowChecked() and setPanFollowChecked() accessors - MainWindow: call setPanFollow(true) after wiring panFollowToggled so the persisted state actually engages tracking on launch - MainWindow: add m_panFollowSliceConn to re-attach frequency tracking whenever slice 0 is destroyed and recreated (radio reconnect, etc.); uncheck button if slice 0 is null to keep UI in sync with reality --- src/gui/MainWindow.cpp | 52 ++++++++++++++++++++++++++++++------------ src/gui/MainWindow.h | 1 + src/gui/TitleBar.h | 2 ++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 90bc87a15..797fe1dc0 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3467,6 +3467,7 @@ MainWindow::MainWindow(QWidget* parent) // ── Title bar: Pan Follow ──────────────────────────────────────────────── connect(m_titleBar, &TitleBar::panFollowToggled, this, &MainWindow::setPanFollow); + if (m_titleBar->isPanFollowChecked()) setPanFollow(true); // ── Title bar: PC Audio, master volume, headphone volume ──────────────── // The remote_audio_rx stream controls the radio's audio routing: @@ -19135,26 +19136,47 @@ void MainWindow::onSpectrumReadyForSHistory(quint32 streamId, const QVectorpanId(); - if (panId.isEmpty()) return; - const double freq = s->frequency(); - auto* pan = m_radioModel.panadapter(panId); - if (pan && qFuzzyCompare(pan->centerMhz(), freq)) return; - const QString freqStr = QString::number(freq, 'f', 6); - if (pan) pan->applyPanStatus({{"center", freqStr}}); - m_radioModel.sendCommand( - QString("display pan set %1 center=%2").arg(panId, freqStr)); + auto* s = m_radioModel.slice(0); + if (!s) { + // No slice yet — uncheck the button so UI matches reality. + if (m_titleBar) m_titleBar->setPanFollowChecked(false); + return; + } + + auto centerPan = [this, s]() { + const QString panId = s->panId(); + if (panId.isEmpty()) return; + const double freq = s->frequency(); + auto* pan = m_radioModel.panadapter(panId); + if (pan && qFuzzyCompare(pan->centerMhz(), freq)) return; + const QString freqStr = QString::number(freq, 'f', 6); + if (pan) pan->applyPanStatus({{"center", freqStr}}); + m_radioModel.sendCommand( + QString("display pan set %1 center=%2").arg(panId, freqStr)); + }; + + centerPan(); + m_panFollowConn = connect(s, &SliceModel::frequencyChanged, + this, [centerPan](double) { centerPan(); }); }; - centerPan(); - m_panFollowConn = connect(s, &SliceModel::frequencyChanged, - this, [centerPan](double) { centerPan(); }); + attachToSlice0(); + + // Re-attach whenever a new slice 0 appears (reconnect / re-assignment). + m_panFollowSliceConn = connect(&m_radioModel, &RadioModel::sliceAdded, + this, [this, attachToSlice0](SliceModel* s) { + if (s && s->sliceId() == 0) attachToSlice0(); + }); } } // namespace AetherSDR diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 4c2d8f6ea..e612a0ffe 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -931,6 +931,7 @@ private slots: // Pan Follow — keeps the panadapter centered on Slice A frequency QMetaObject::Connection m_panFollowConn; + QMetaObject::Connection m_panFollowSliceConn; void setPanFollow(bool on); #if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE) diff --git a/src/gui/TitleBar.h b/src/gui/TitleBar.h index 825ab38c2..7df80b51c 100644 --- a/src/gui/TitleBar.h +++ b/src/gui/TitleBar.h @@ -53,6 +53,8 @@ class TitleBar : public QWidget { signals: void panFollowToggled(bool on); + bool isPanFollowChecked() const { return m_panFollowBtn && m_panFollowBtn->isChecked(); } + void setPanFollowChecked(bool on) { if (m_panFollowBtn) { QSignalBlocker b(m_panFollowBtn); m_panFollowBtn->setChecked(on); } } void pcAudioToggled(bool on); void masterVolumeChanged(int pct); void headphoneVolumeChanged(int pct); From 932f4116a4368c905ea2139062ea218b42d16898 Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sat, 6 Jun 2026 16:12:17 -0700 Subject: [PATCH 5/6] fix(gui): move PanLock accessors out of signals: block so the PR builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes to TitleBar's new PanLock accessors so all four platforms compile again: 1. signals: vs. public: — MOC parses anything inside a `signals:` block as a signal declaration, so the inline `bool isPanFollowChecked() const` and `void setPanFollowChecked(bool)` were rejected at moc time with "Not a signal declaration" (TitleBar.h:56). Both methods are accessors, not signals — they belong in `public:`. 2. Out-of-line over inline. Even after the section move, the inline bodies referenced `m_panFollowBtn->isChecked()` and `QSignalBlocker`, which need the full QPushButton type. The header only forward-declares QPushButton, so inline bodies couldn't compile in callers either (the moc error masked this until now). Moving the definitions to TitleBar.cpp keeps the header light and matches the style of neighbours like `isSystemMoveAreaAt`. Build verified locally on Arch Linux x86 — 632/632 clean (only the pre-existing unrelated macDaxDriverInstalled warning). Same maintainer fix-up pattern as #3279/#3286/#3289/#3381/#3398/#3417/#3439/#3441. Principle XI. --- src/gui/TitleBar.cpp | 13 +++++++++++++ src/gui/TitleBar.h | 7 +++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/gui/TitleBar.cpp b/src/gui/TitleBar.cpp index ed10fd931..2557185ee 100644 --- a/src/gui/TitleBar.cpp +++ b/src/gui/TitleBar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -491,6 +492,18 @@ bool TitleBar::isDragHandle(QObject* obj) const return obj && obj->property(kTitleDragHandleProperty).toBool(); } +bool TitleBar::isPanFollowChecked() const +{ + return m_panFollowBtn && m_panFollowBtn->isChecked(); +} + +void TitleBar::setPanFollowChecked(bool on) +{ + if (!m_panFollowBtn) return; + QSignalBlocker block(m_panFollowBtn); + m_panFollowBtn->setChecked(on); +} + bool TitleBar::isSystemMoveAreaAt(const QPoint& globalPos) const { const QPoint localPos = mapFromGlobal(globalPos); diff --git a/src/gui/TitleBar.h b/src/gui/TitleBar.h index 7df80b51c..1dfc3c2ad 100644 --- a/src/gui/TitleBar.h +++ b/src/gui/TitleBar.h @@ -51,10 +51,13 @@ class TitleBar : public QWidget { // as caption drag zones while keeping controls interactive. bool isSystemMoveAreaAt(const QPoint& globalPos) const; + // Pan Lock accessors. Out-of-line so the header doesn't need to pull in + // ; defined alongside the button's setup in TitleBar.cpp. + bool isPanFollowChecked() const; + void setPanFollowChecked(bool on); + signals: void panFollowToggled(bool on); - bool isPanFollowChecked() const { return m_panFollowBtn && m_panFollowBtn->isChecked(); } - void setPanFollowChecked(bool on) { if (m_panFollowBtn) { QSignalBlocker b(m_panFollowBtn); m_panFollowBtn->setChecked(on); } } void pcAudioToggled(bool on); void masterVolumeChanged(int pct); void headphoneVolumeChanged(int pct); From e27f2d228f4beabbcc170b32f2f79221aeee29a6 Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sat, 6 Jun 2026 17:54:39 -0700 Subject: [PATCH 6/6] fix(gui): migrate PanLockEnabled to nested JSON (Principle V) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat AppSettings key "PanLockEnabled" added in this PR violated Constitution Principle V (nested-JSON-per-feature config). New TitleBarSettings.h header-only helper mirrors the established DisplaySettings.h / CwDecodeSettings.h pattern — single AppSettings root "TitleBar" holding a nested JSON blob with feature toggles, with a one-shot migrateLegacy() that reads the legacy flat key on first launch and writes it into the new blob. TitleBar.cpp now reads via TitleBarSettings::panLockEnabled() and writes via TitleBarSettings::setPanLockEnabled(bool); the migration is called once at the top of the TitleBar constructor before any other title-bar-settings reader runs. The legacy "PanLockEnabled" flat key is intentionally left in place — harmless after migration, and the same future-cleanup-PR pattern as Lean Mode's "LeanMode" key (DisplaySettings.h). Settings key shape after this commit: TitleBar = { "panLockEnabled": "True"|"False" } Future title-bar toggles (heartbeat blink, etc.) can land as additional fields in the same blob without re-introducing flat keys. Verified locally on Arch Linux x86 — full AetherSDR build clean (634/635, only the pre-existing unrelated macDaxDriverInstalled warning). Principle V. --- src/gui/TitleBar.cpp | 13 +++---- src/gui/TitleBarSettings.h | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/gui/TitleBarSettings.h diff --git a/src/gui/TitleBar.cpp b/src/gui/TitleBar.cpp index 2557185ee..0746b4935 100644 --- a/src/gui/TitleBar.cpp +++ b/src/gui/TitleBar.cpp @@ -1,5 +1,6 @@ #include "TitleBar.h" #include "GuardedSlider.h" +#include "TitleBarSettings.h" #include "core/AppSettings.h" #include @@ -220,11 +221,13 @@ TitleBar::TitleBar(QWidget* parent) // ── PC Audio + Master Vol + HP Vol ────────────────────────────────────── auto& s = AppSettings::instance(); - // Pan Follow toggle — keeps the panadapter centered on Slice A frequency + // Pan Follow toggle — keeps the panadapter centered on Slice A frequency. + // Persistence is the nested "TitleBar" blob (Principle V); the legacy + // flat "PanLockEnabled" key is migrated into it on first read. + TitleBarSettings::migrateLegacy(); m_panFollowBtn = new QPushButton("Pan Lock"); m_panFollowBtn->setCheckable(true); - bool panLockOn = s.value("PanLockEnabled", "False").toString() == "True"; - m_panFollowBtn->setChecked(panLockOn); + m_panFollowBtn->setChecked(TitleBarSettings::panLockEnabled()); m_panFollowBtn->setFixedHeight(22); m_panFollowBtn->setFixedWidth(70); m_panFollowBtn->setAccessibleName("Pan follow slice"); @@ -244,9 +247,7 @@ TitleBar::TitleBar(QWidget* parent) connect(m_panFollowBtn, &QPushButton::toggled, this, [this, updatePanFollowStyle](bool on) { updatePanFollowStyle(); - auto& ss = AppSettings::instance(); - ss.setValue("PanLockEnabled", on ? "True" : "False"); - ss.save(); + TitleBarSettings::setPanLockEnabled(on); emit panFollowToggled(on); }); m_hbox->addWidget(m_panFollowBtn); diff --git a/src/gui/TitleBarSettings.h b/src/gui/TitleBarSettings.h new file mode 100644 index 000000000..cb06afe49 --- /dev/null +++ b/src/gui/TitleBarSettings.h @@ -0,0 +1,69 @@ +#pragma once + +#include "core/AppSettings.h" + +#include +#include +#include + +namespace AetherSDR { + +// Persistence helper for title-bar UI toggles (#3408 PanLock is the first; +// future title-bar toggles land here as additional fields). +// +// Stored as a nested JSON blob under AppSettings["TitleBar"], per the +// nested-JSON-per-feature convention (constitution Principle V). The legacy +// flat key "PanLockEnabled" is migrated into this blob on first read so +// existing users keep their behavior. +class TitleBarSettings { +public: + static bool panLockEnabled() + { + return readObj().value("panLockEnabled").toString("False") == "True"; + } + + static void setPanLockEnabled(bool on) + { + QJsonObject o = readObj(); + o["panLockEnabled"] = on ? QStringLiteral("True") : QStringLiteral("False"); + write(o); + } + + // One-shot migration from the legacy "PanLockEnabled" flat key. Run at app + // startup (or TitleBar construction) before any caller reads the new blob. + // Safe to call repeatedly: returns immediately if the new blob already + // exists. + static void migrateLegacy() + { + auto& s = AppSettings::instance(); + if (s.contains("TitleBar")) return; + const bool legacyPanLock = + s.value("PanLockEnabled", "False").toString() == "True"; + QJsonObject o; + o["panLockEnabled"] = + legacyPanLock ? QStringLiteral("True") : QStringLiteral("False"); + write(o); + // Leave the legacy flat key in place — harmless after migration, and a + // future cleanup PR can drop it once we're confident no other reader + // still touches it. + } + +private: + static QJsonObject readObj() + { + const QString json = + AppSettings::instance().value("TitleBar", QString{}).toString(); + if (json.isEmpty()) return {}; + return QJsonDocument::fromJson(json.toUtf8()).object(); + } + static void write(const QJsonObject& o) + { + auto& s = AppSettings::instance(); + s.setValue("TitleBar", + QString::fromUtf8( + QJsonDocument(o).toJson(QJsonDocument::Compact))); + s.save(); + } +}; + +} // namespace AetherSDR