From be01b7e835442a1dbeb1d15b0cb35bfe9198dd82 Mon Sep 17 00:00:00 2001 From: jensenpat Date: Mon, 13 Jul 2026 22:38:05 -0700 Subject: [PATCH 1/3] Make VFO DSP label reflect active noise reduction. Principle XI. Fixes #4237 --- src/gui/VfoWidget.cpp | 108 +++++++++++++++++++++++++++++++++--------- src/gui/VfoWidget.h | 1 + 2 files changed, 86 insertions(+), 23 deletions(-) diff --git a/src/gui/VfoWidget.cpp b/src/gui/VfoWidget.cpp index 332b0bd18..eabcbd9c6 100644 --- a/src/gui/VfoWidget.cpp +++ b/src/gui/VfoWidget.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include #include @@ -256,6 +255,12 @@ static const QString kTabLblActive = "color: #00b4d8; font-size: 13px; font-weight: bold; padding: 3px 0; }" "QPushButton:focus { outline: none; }"; +static const QString kTabLblDspActive = + "QPushButton { background: transparent; border: none; " + "border-bottom: 2px solid #20a040; " + "color: #20a040; font-size: 13px; font-weight: bold; padding: 3px 0; }" + "QPushButton:focus { outline: none; }"; + static const QString kDisabledBtn = "QPushButton:disabled { background-color: #1a1a2a; color: #556070; " "border: 1px solid #2a3040; }"; @@ -1248,6 +1253,9 @@ void VfoWidget::buildUI() btn->setFixedHeight(24); btn->setCursor(Qt::PointingHandCursor); btn->setFocusPolicy(Qt::TabFocus); + if (i == 1) { + btn->setObjectName(QStringLiteral("dspTabButton")); + } connect(btn, &QPushButton::clicked, this, [this, i]() { showTab(i); }); if (i == 0) { // Right-click on speaker tab toggles mute directly @@ -1743,7 +1751,7 @@ void VfoWidget::buildTabContent() m_aetherDspBtn = new QPushButton("ADSP"); m_aetherDspBtn->setObjectName("aetherDspBtn"); m_aetherDspBtn->setCheckable(false); - m_aetherDspBtn->setMinimumHeight(22); + m_aetherDspBtn->setFixedHeight(26); m_aetherDspBtn->setStyleSheet(kDspToggle); m_aetherDspBtn->setAccessibleName("AetherDSP Settings"); m_aetherDspBtn->setToolTip("Open AetherDSP Settings (client-side NR2 / NR4 / DFNR / RN2 / BNR / MNR)"); @@ -1754,7 +1762,7 @@ void VfoWidget::buildTabContent() // 2 columns wide (cols 2-3 of the same row that hosts ADSP). m_aetherVoiceBtn = new QPushButton("AetherVoice"); m_aetherVoiceBtn->setCheckable(false); - m_aetherVoiceBtn->setMinimumHeight(22); + m_aetherVoiceBtn->setFixedHeight(26); m_aetherVoiceBtn->setStyleSheet(kDspToggle); m_aetherVoiceBtn->setAccessibleName("Aetherial Audio Channel Strip"); m_aetherVoiceBtn->setToolTip("Open Aetherial Audio Channel Strip — unified TX DSP suite"); @@ -1818,15 +1826,10 @@ void VfoWidget::buildTabContent() } }); - // Stay laid out always — toggling visibility would shift the - // button grid up/down each time the slider's target changes. - // Instead, keep the row in the layout and fade contents with - // an opacity effect (0 when no DSP is targeted, 1 when one - // is). Stray clicks while transparent are no-ops because - // the slider's valueChanged handler ignores LvlNone. - auto* eff = new QGraphicsOpacityEffect(m_dspLevelRow); - eff->setOpacity(0.0); - m_dspLevelRow->setGraphicsEffect(eff); + // Keep the panel compact when no leveled DSP is active. The row + // appears only when it has a real target; setDspLevelTarget() + // refits the flag after each visibility transition. + m_dspLevelRow->hide(); dspVb->addWidget(m_dspLevelRow); } @@ -2551,11 +2554,16 @@ void VfoWidget::closeActiveTab() if (m_tabStack) { m_tabStack->hide(); } - if (m_activeTab < m_tabBtns.size()) { - m_tabBtns[m_activeTab]->setStyleSheet(kTabLblNormal); - m_tabBtns[m_activeTab]->setChecked(false); + const int closedTab = m_activeTab; + if (closedTab < m_tabBtns.size()) { + m_tabBtns[closedTab]->setChecked(false); } m_activeTab = -1; + if (closedTab == 1) { + updateDspTabAccent(); + } else if (closedTab < m_tabBtns.size()) { + m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); + } } // Open or close the S-Meter / SmartMTR selector. Single source of truth for @@ -2594,14 +2602,25 @@ void VfoWidget::showTab(int index) { if (m_activeTab == index) { // Toggle off — collapse content + const int closedTab = m_activeTab; m_tabStack->hide(); - m_tabBtns[m_activeTab]->setStyleSheet(kTabLblNormal); - m_tabBtns[m_activeTab]->setChecked(false); + m_tabBtns[closedTab]->setChecked(false); m_activeTab = -1; + if (closedTab == 1) { + updateDspTabAccent(); + } else { + m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); + } } else { if (m_activeTab >= 0) { - m_tabBtns[m_activeTab]->setStyleSheet(kTabLblNormal); - m_tabBtns[m_activeTab]->setChecked(false); + const int closedTab = m_activeTab; + m_tabBtns[closedTab]->setChecked(false); + m_activeTab = -1; + if (closedTab == 1) { + updateDspTabAccent(); + } else { + m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); + } } m_activeTab = index; m_tabBtns[index]->setStyleSheet(kTabLblActive); @@ -2616,6 +2635,38 @@ void VfoWidget::showTab(int index) relayoutToCurrentContent(); } +void VfoWidget::updateDspTabAccent() +{ + if (m_tabBtns.size() <= 1) { + return; + } + + const bool radioDspActive = m_slice + && (m_slice->nbOn() || m_slice->nrOn() || m_slice->anfOn() + || m_slice->nrlOn() || m_slice->nrsOn() || m_slice->rnnOn() + || m_slice->nrfOn() || m_slice->anflOn() || m_slice->anftOn() + || m_slice->apfOn()); + const bool noiseReductionActive = radioDspActive || m_aetherDspActive; + QPushButton* dspTabButton = m_tabBtns[1]; + + const QString accessibleName = noiseReductionActive + ? tr("DSP settings (noise reduction active)") + : tr("DSP settings"); + if (dspTabButton->accessibleName() != accessibleName) { + dspTabButton->setAccessibleName(accessibleName); + QAccessibleEvent event(dspTabButton, QAccessible::NameChanged); + QAccessible::updateAccessibility(&event); + } + + // Cyan remains the unambiguous open-panel state. Green is the persistent + // closed-panel cue that at least one radio or client DSP is engaged. + if (m_activeTab != 1) { + dspTabButton->setStyleSheet(noiseReductionActive + ? kTabLblDspActive + : kTabLblNormal); + } +} + void VfoWidget::setCollapsed(bool collapsed) { if (m_collapsed == collapsed) return; @@ -2712,6 +2763,7 @@ void VfoWidget::setCollapsed(bool collapsed) btn->setStyleSheet(kTabLblNormal); btn->setChecked(false); } + updateDspTabAccent(); } // Restore external buttons to pre-collapse state and reposition them // based on the new expanded width (they were positioned for COLLAPSED_W) @@ -2938,6 +2990,7 @@ void VfoWidget::setAetherDspActive(bool active) m_aetherDspBtn->setStyleSheet(active ? kDspToggleActive : kDspToggle); m_aetherDspBtn->setAccessibleName(active ? QStringLiteral("AetherDSP Settings (NR active)") : QStringLiteral("AetherDSP Settings")); + updateDspTabAccent(); } // ── Per-slice VFO marker display prefs (#1526) ─────────────────────────────── @@ -4133,6 +4186,7 @@ void VfoWidget::setSlice(SliceModel* slice) QSignalBlocker sb(btn); btn->setChecked(on); m_updatingFromModel = false; + updateDspTabAccent(); }); }; // Leveled variant — also push/pop the shared DSP-level slider stack @@ -4148,6 +4202,7 @@ void VfoWidget::setSlice(SliceModel* slice) m_updatingFromModel = false; if (on) pushDspLevelTarget(tag); else popDspLevelTarget(tag); + updateDspTabAccent(); }); }; connectLeveledDsp(&SliceModel::nbChanged, m_nbBtn, LvlNB); @@ -4537,6 +4592,7 @@ void VfoWidget::syncFromSlice() syncDsp(m_anflBtn, m_slice->anflOn()); syncDsp(m_anftBtn, m_slice->anftOn()); syncDsp(m_apfBtn, m_slice->apfOn()); + updateDspTabAccent(); // Shared DSP-level slider — pick the highest-priority enabled DSP. refreshDspLevelTarget(); @@ -4728,9 +4784,13 @@ void VfoWidget::setDspLevelTarget(DspLevelTarget t) { m_dspLevelTarget = t; if (!m_dspLevelRow) return; - auto* eff = qobject_cast(m_dspLevelRow->graphicsEffect()); - if (t == LvlNone || !m_slice) { - if (eff) eff->setOpacity(0.0); + const bool showLevelRow = t != LvlNone && m_slice; + const bool visibilityChanged = m_dspLevelRow->isHidden() == showLevelRow; + m_dspLevelRow->setVisible(showLevelRow); + if (!showLevelRow) { + if (visibilityChanged && m_activeTab == 1 && m_tabStack->isVisible()) { + QTimer::singleShot(0, this, [this] { relayoutToCurrentContent(); }); + } return; } int level = 0; @@ -4751,7 +4811,9 @@ void VfoWidget::setDspLevelTarget(DspLevelTarget t) m_dspLevelSlider->setValue(level); } m_dspLevelValue->setText(QString::number(level)); - if (eff) eff->setOpacity(1.0); + if (visibilityChanged && m_activeTab == 1 && m_tabStack->isVisible()) { + QTimer::singleShot(0, this, [this] { relayoutToCurrentContent(); }); + } } void VfoWidget::refreshDspLevelTarget() diff --git a/src/gui/VfoWidget.h b/src/gui/VfoWidget.h index 4824256ee..6b00a3c1c 100644 --- a/src/gui/VfoWidget.h +++ b/src/gui/VfoWidget.h @@ -383,6 +383,7 @@ class VfoWidget : public QWidget { void updateTxBadgeStyle(bool isTx); void showTab(int index); void closeActiveTab(); // close any open DSP/Mode/... tab panel + void updateDspTabAccent(); void updateFreqLabel(); bool cancelDirectEntry(); void updateFilterLabel(); From 9bdd89ac8ef7da6a88b46d0ba64fbf9181dff788 Mon Sep 17 00:00:00 2001 From: jensenpat Date: Fri, 17 Jul 2026 10:07:54 -0700 Subject: [PATCH 2/3] Gate VFO DSP accent by available controls. Principle XI. --- src/gui/VfoWidget.cpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/gui/VfoWidget.cpp b/src/gui/VfoWidget.cpp index eabcbd9c6..648d0cfa8 100644 --- a/src/gui/VfoWidget.cpp +++ b/src/gui/VfoWidget.cpp @@ -2641,16 +2641,25 @@ void VfoWidget::updateDspTabAccent() return; } + const auto activeWhenAvailable = [](const QPushButton* button, bool active) { + return active && button && !button->isHidden(); + }; const bool radioDspActive = m_slice - && (m_slice->nbOn() || m_slice->nrOn() || m_slice->anfOn() - || m_slice->nrlOn() || m_slice->nrsOn() || m_slice->rnnOn() - || m_slice->nrfOn() || m_slice->anflOn() || m_slice->anftOn() - || m_slice->apfOn()); - const bool noiseReductionActive = radioDspActive || m_aetherDspActive; + && (activeWhenAvailable(m_nbBtn, m_slice->nbOn()) + || activeWhenAvailable(m_nrBtn, m_slice->nrOn()) + || activeWhenAvailable(m_anfBtn, m_slice->anfOn()) + || activeWhenAvailable(m_nrlBtn, m_slice->nrlOn()) + || activeWhenAvailable(m_nrsBtn, m_slice->nrsOn()) + || activeWhenAvailable(m_rnnBtn, m_slice->rnnOn()) + || activeWhenAvailable(m_nrfBtn, m_slice->nrfOn()) + || activeWhenAvailable(m_anflBtn, m_slice->anflOn()) + || activeWhenAvailable(m_anftBtn, m_slice->anftOn()) + || activeWhenAvailable(m_apfBtn, m_slice->apfOn())); + const bool dspActive = radioDspActive || m_aetherDspActive; QPushButton* dspTabButton = m_tabBtns[1]; - const QString accessibleName = noiseReductionActive - ? tr("DSP settings (noise reduction active)") + const QString accessibleName = dspActive + ? tr("DSP settings (DSP active)") : tr("DSP settings"); if (dspTabButton->accessibleName() != accessibleName) { dspTabButton->setAccessibleName(accessibleName); @@ -2661,7 +2670,7 @@ void VfoWidget::updateDspTabAccent() // Cyan remains the unambiguous open-panel state. Green is the persistent // closed-panel cue that at least one radio or client DSP is engaged. if (m_activeTab != 1) { - dspTabButton->setStyleSheet(noiseReductionActive + dspTabButton->setStyleSheet(dspActive ? kTabLblDspActive : kTabLblNormal); } @@ -4077,6 +4086,7 @@ void VfoWidget::setSlice(SliceModel* slice) m_nrlBtn->setVisible(!isFm); // 8000-series-only firmware DSP filters — shared rule (#2177) updateExtendedDspVisibility(); + updateDspTabAccent(); relayoutDspGrid(); updateFilterLabel(); if (m_tabStack->isVisible()) relayoutToCurrentContent(); @@ -4592,7 +4602,6 @@ void VfoWidget::syncFromSlice() syncDsp(m_anflBtn, m_slice->anflOn()); syncDsp(m_anftBtn, m_slice->anftOn()); syncDsp(m_apfBtn, m_slice->apfOn()); - updateDspTabAccent(); // Shared DSP-level slider — pick the highest-priority enabled DSP. refreshDspLevelTarget(); @@ -4613,7 +4622,8 @@ void VfoWidget::syncFromSlice() m_rttyContainer->setVisible(isRtty); bool isCw = (m_slice->mode() == "CW" || m_slice->mode() == "CWL"); bool isDig = (m_slice->mode() == "DIGL" || m_slice->mode() == "DIGU" || m_slice->mode() == "NT"); - bool isFm = (m_slice->mode() == "FM" || m_slice->mode() == "NFM"); + bool isFm = (m_slice->mode() == "FM" || m_slice->mode() == "NFM" + || m_slice->mode() == "DFM"); m_tabBtns[1]->setText(isFm ? "OPT" : "DSP"); m_apfBtn->setVisible(isCw); m_anfBtn->setVisible(!isRtty && !isCw && !isDig && !isFm); @@ -4650,6 +4660,7 @@ void VfoWidget::syncFromSlice() m_digOffsetLabel->setText(QString::number(off)); } relayoutDspGrid(); + updateDspTabAccent(); // APF level { From c28605f31d372f5c0510713dcc0bedcf77877e6b Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sat, 18 Jul 2026 16:42:35 -0700 Subject: [PATCH 3/3] Address review nits: guard DSP accent repaint, dedupe tab deactivation (#4241) Three non-blocking review nits from @NF0T: - updateDspTabAccent()'s setStyleSheet() now skips the repaint when the target style is unchanged, matching the accessible-name guard above it. One radio status packet re-runs this up to ~9x (SliceModel::applyChanges emits the DSP signals unconditionally), so the unguarded style recompute was wasted work on a per-packet path. - Extract deactivateTabButton(int) to replace the three copy-pasted "reset a just-closed tab button" blocks in closeActiveTab() and both showTab() branches, centralizing the "m_activeTab already cleared" precondition. - Drop the unused setObjectName("dspTabButton") scaffolding (no consumer in the repo). No behavior change. Full app builds clean; offscreen smoke-launch constructs the VFO widget without asserts. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gui/VfoWidget.cpp | 54 ++++++++++++++++++++++--------------------- src/gui/VfoWidget.h | 1 + 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/gui/VfoWidget.cpp b/src/gui/VfoWidget.cpp index 648d0cfa8..7f220216d 100644 --- a/src/gui/VfoWidget.cpp +++ b/src/gui/VfoWidget.cpp @@ -1253,9 +1253,6 @@ void VfoWidget::buildUI() btn->setFixedHeight(24); btn->setCursor(Qt::PointingHandCursor); btn->setFocusPolicy(Qt::TabFocus); - if (i == 1) { - btn->setObjectName(QStringLiteral("dspTabButton")); - } connect(btn, &QPushButton::clicked, this, [this, i]() { showTab(i); }); if (i == 0) { // Right-click on speaker tab toggles mute directly @@ -2555,15 +2552,8 @@ void VfoWidget::closeActiveTab() m_tabStack->hide(); } const int closedTab = m_activeTab; - if (closedTab < m_tabBtns.size()) { - m_tabBtns[closedTab]->setChecked(false); - } m_activeTab = -1; - if (closedTab == 1) { - updateDspTabAccent(); - } else if (closedTab < m_tabBtns.size()) { - m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); - } + deactivateTabButton(closedTab); } // Open or close the S-Meter / SmartMTR selector. Single source of truth for @@ -2604,23 +2594,13 @@ void VfoWidget::showTab(int index) // Toggle off — collapse content const int closedTab = m_activeTab; m_tabStack->hide(); - m_tabBtns[closedTab]->setChecked(false); m_activeTab = -1; - if (closedTab == 1) { - updateDspTabAccent(); - } else { - m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); - } + deactivateTabButton(closedTab); } else { if (m_activeTab >= 0) { const int closedTab = m_activeTab; - m_tabBtns[closedTab]->setChecked(false); m_activeTab = -1; - if (closedTab == 1) { - updateDspTabAccent(); - } else { - m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); - } + deactivateTabButton(closedTab); } m_activeTab = index; m_tabBtns[index]->setStyleSheet(kTabLblActive); @@ -2670,9 +2650,31 @@ void VfoWidget::updateDspTabAccent() // Cyan remains the unambiguous open-panel state. Green is the persistent // closed-panel cue that at least one radio or client DSP is engaged. if (m_activeTab != 1) { - dspTabButton->setStyleSheet(dspActive - ? kTabLblDspActive - : kTabLblNormal); + // Guard the repaint like the accessible name above: a single radio + // status packet re-runs this up to 9x (SliceModel::applyChanges emits + // the DSP signals unconditionally), and setStyleSheet() forces a full + // style recompute even when the accent is unchanged. + const QString& target = dspActive ? kTabLblDspActive : kTabLblNormal; + if (dspTabButton->styleSheet() != target) { + dspTabButton->setStyleSheet(target); + } + } +} + +// Drop a just-closed tab button back to its resting style. The caller must +// already have cleared m_activeTab so the DSP tab (index 1) resolves its +// persistent closed-panel accent here; every other tab returns to the plain +// label style. +void VfoWidget::deactivateTabButton(int closedTab) +{ + if (closedTab < 0 || closedTab >= m_tabBtns.size()) { + return; + } + m_tabBtns[closedTab]->setChecked(false); + if (closedTab == 1) { + updateDspTabAccent(); + } else { + m_tabBtns[closedTab]->setStyleSheet(kTabLblNormal); } } diff --git a/src/gui/VfoWidget.h b/src/gui/VfoWidget.h index 6b00a3c1c..e93ff8850 100644 --- a/src/gui/VfoWidget.h +++ b/src/gui/VfoWidget.h @@ -384,6 +384,7 @@ class VfoWidget : public QWidget { void showTab(int index); void closeActiveTab(); // close any open DSP/Mode/... tab panel void updateDspTabAccent(); + void deactivateTabButton(int closedTab); // reset a just-closed tab's style void updateFreqLabel(); bool cancelDirectEntry(); void updateFilterLabel();