From f99197dbac2d9b640aeb39961e072d9e527b0e86 Mon Sep 17 00:00:00 2001 From: jensenpat Date: Thu, 16 Jul 2026 09:03:47 -0700 Subject: [PATCH 1/2] Fix CWX availability for the TX slice. Principle I. Keep F1-F12 shortcut routing tied to the selected slice while deriving CWX availability from the radio TX slice. Refresh the availability gate when TX assignment or either relevant slice mode changes. Fixes #4173 --- src/gui/MainWindow.cpp | 42 ++++++++++++++++++++++------------- src/gui/MainWindow.h | 2 +- src/gui/MainWindow_Wiring.cpp | 13 ++++++++--- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 2f12ceb41..4c2924070 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -7853,46 +7853,58 @@ void MainWindow::showPanadapterInterlockNotification(const QString& message, // ─── Keyboard Shortcuts ─────────────────────────────────────────────────────── -void MainWindow::updateKeyerAvailability(const QString& mode) +void MainWindow::updateKeyerAvailability(const QString& activeMode) { static const QString kActive = "QLabel { color: #00b4d8; font-weight: bold; font-size: 24px; }"; static const QString kAvail = "QLabel { color: #404858; font-weight: bold; font-size: 24px; }"; static const QString kDisabled = "QLabel { color: #252530; font-weight: bold; font-size: 24px; }"; - bool isCw = (mode == "CW" || mode == "CWL"); - bool isSsb = (mode == "USB" || mode == "LSB" || mode == "AM" || mode == "SAM" - || mode == "FM" || mode == "NFM" || mode == "DFM"); + const bool activeIsCw = (activeMode == "CW" || activeMode == "CWL"); + const bool activeIsSsb = (activeMode == "USB" || activeMode == "LSB" + || activeMode == "AM" || activeMode == "SAM" + || activeMode == "FM" || activeMode == "NFM" + || activeMode == "DFM"); + + QString txMode; + for (SliceModel* slice : m_radioModel.slices()) { + if (slice && slice->isTxSlice()) { + txMode = slice->mode(); + break; + } + } + const bool txIsCw = (txMode == "CW" || txMode == "CWL"); // F1-F12 / Esc ApplicationShortcuts: enable the set that matches the // active slice's mode, regardless of panel visibility. The two sets // are mutually exclusive so Qt never sees two enabled shortcuts for // the same key and won't emit activatedAmbiguously (#2464, #2582). - if (m_cwxPanel) m_cwxPanel->setShortcutsEnabled(isCw); - if (m_dvkPanel) m_dvkPanel->setShortcutsEnabled(isSsb); + if (m_cwxPanel) m_cwxPanel->setShortcutsEnabled(activeIsCw); + if (m_dvkPanel) m_dvkPanel->setShortcutsEnabled(activeIsSsb); - // CWX: available in CW modes only - m_cwxIndicator->setEnabled(isCw); - if (!isCw && m_cwxPanel->isVisible()) { + // CWX sends on the radio's TX slice (FlexLib CWX::getTXFrequency), so + // availability follows that slice rather than the selected RX slice. + m_cwxIndicator->setEnabled(txIsCw); + if (!txIsCw && m_cwxPanel->isVisible()) { m_cwxPanel->hide(); m_cwxIndicator->setStyleSheet(kDisabled); } else if (m_cwxPanel->isVisible()) { m_cwxIndicator->setStyleSheet(kActive); } else { - m_cwxIndicator->setStyleSheet(isCw ? kAvail : kDisabled); + m_cwxIndicator->setStyleSheet(txIsCw ? kAvail : kDisabled); } - m_cwxIndicator->setCursor(isCw ? Qt::PointingHandCursor : Qt::ArrowCursor); + m_cwxIndicator->setCursor(txIsCw ? Qt::PointingHandCursor : Qt::ArrowCursor); // DVK: available in voice modes (SSB, AM, FM — not DIGU/DIGL) - m_dvkIndicator->setEnabled(isSsb); - if (!isSsb && m_dvkPanel->isVisible()) { + m_dvkIndicator->setEnabled(activeIsSsb); + if (!activeIsSsb && m_dvkPanel->isVisible()) { m_dvkPanel->hide(); m_dvkIndicator->setStyleSheet(kDisabled); } else if (m_dvkPanel->isVisible()) { m_dvkIndicator->setStyleSheet(kActive); } else { - m_dvkIndicator->setStyleSheet(isSsb ? kAvail : kDisabled); + m_dvkIndicator->setStyleSheet(activeIsSsb ? kAvail : kDisabled); } - m_dvkIndicator->setCursor(isSsb ? Qt::PointingHandCursor : Qt::ArrowCursor); + m_dvkIndicator->setCursor(activeIsSsb ? Qt::PointingHandCursor : Qt::ArrowCursor); } void MainWindow::centerActiveSliceInPanadapter(bool forceRadioCenter, double centerMhz) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 48e8a5d73..966ac08d4 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -589,7 +589,7 @@ private slots: #ifdef HAVE_WEBSOCKETS void showFreeDvReporter(); #endif - void updateKeyerAvailability(const QString& mode); + void updateKeyerAvailability(const QString& activeMode); void showNr2ParamPopup(const QPoint& globalPos); void showNr4ParamPopup(const QPoint& globalPos); void showDfnrParamPopup(const QPoint& globalPos); diff --git a/src/gui/MainWindow_Wiring.cpp b/src/gui/MainWindow_Wiring.cpp index a2f3ca7c6..57cf42e3b 100644 --- a/src/gui/MainWindow_Wiring.cpp +++ b/src/gui/MainWindow_Wiring.cpp @@ -1328,6 +1328,9 @@ void MainWindow::onSliceAdded(SliceModel* s) syncTxWaterfallSliceToSpectrums(); updateSplitState(); + SliceModel* active = activeSlice(); + updateKeyerAvailability(active ? active->mode() : QString()); + // Active follows TX slice (#1351) — switch the displayed/active slice // when an external program (e.g. WSJT-X) moves the TX flag if (tx && s->sliceId() != m_activeSliceId @@ -1363,9 +1366,6 @@ void MainWindow::onSliceAdded(SliceModel* s) refreshCwDecodeState(); refreshRttyDecodeState(); - // Update CWX/DVK indicator availability for new mode - updateKeyerAvailability(mode); - // Disable client-side DSP in digital and CW modes — NR2/RN2/BNR // corrupt digital data (#534) and suppress CW tones (#784) bool disableDsp = (mode == "DIGU" || mode == "DIGL" || mode == "RTTY" @@ -1383,6 +1383,13 @@ void MainWindow::onSliceAdded(SliceModel* s) QMetaObject::invokeMethod(m_audio, [this]() { m_audio->setNvAfxEnabled(false); }); } } + + // Shortcuts follow the selected slice, while CWX availability follows + // the TX slice. Re-evaluate when either slice changes mode (#4173). + if (s->sliceId() == m_activeSliceId || s->isTxSlice()) { + SliceModel* active = activeSlice(); + updateKeyerAvailability(active ? active->mode() : QString()); + } #ifdef HAVE_RADE if (mode.startsWith("FDV")) activateFdvDisplay(s->sliceId()); From 66a72d076fcedd7457d66fb3c45060d0aad144f7 Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sat, 18 Jul 2026 17:33:25 -0700 Subject: [PATCH 2/2] fix(cwx): make CWX+DVK keyer availability and shortcuts follow the TX slice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial fix made the CWX *indicator* follow the TX slice but left the F1-F12 shortcut set and the whole DVK path on the *selected* RX slice, which produced two defects: 1. Split VFO (selected=USB, TX=CW): the CWX indicator lit and the panel opened, but F1-F12 no-oped — the CwxPanel handler and setShortcutsEnabled still gated on the selected slice. And because CWX shortcuts derived from the TX slice's mode while DVK shortcuts derived from the selected slice's mode, both F1-F12 sets could be enabled at once (TX=CW + selected=USB), re-opening the activatedAmbiguously hole #2464/#2582 closed. 2. A momentary "no TX slice" — during a TX handoff between slices, or a band-recall that drops+recreates the TX slice — made txIsCw false and hid an open CWX panel with no re-show path, so the panel silently vanished until the operator reopened it. Fix: drive both keyers' availability *and* their F1-F12 shortcuts from the single TX slice. One source keeps the two shortcut sets mutually exclusive (a slice can't be CW and voice at once), and only auto-hide a panel when a TX slice actually exists and is in the wrong mode — a transient absence no longer yanks the panel. Also: - Reuse RadioModel::txSlice() (promoted from private) instead of the hand-rolled slice scan the fix added — dedupes a 3rd copy of the loop. - Point CwxPanel's F1-F12 mode guard at the TX slice (setTxModeProvider). - Cite the Principle I evidence for TX-slice targeting: reference/FlexLib_API_v4.1.5.39794/FlexLib/CWX.cs:186-199 getTXFrequency(). - Collapse the redundant activeMode/txMode split; updateKeyerAvailability() now takes no argument and reads the TX slice itself. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gui/CwxPanel.cpp | 13 +++--- src/gui/CwxPanel.h | 10 +++-- src/gui/MainWindow.cpp | 72 ++++++++++++++++---------------- src/gui/MainWindow.h | 2 +- src/gui/MainWindow_Shortcuts.cpp | 6 +-- src/gui/MainWindow_Wiring.cpp | 14 +++---- src/models/RadioModel.h | 5 ++- 7 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/gui/CwxPanel.cpp b/src/gui/CwxPanel.cpp index 9ec094fe5..e8acd7531 100644 --- a/src/gui/CwxPanel.cpp +++ b/src/gui/CwxPanel.cpp @@ -284,12 +284,13 @@ CwxPanel::CwxPanel(CwxModel* model, QWidget* parent) this, [this](int v) { if (m_model) m_model->setSpeed(v); }); // Wire model signals - // ── F1-F12 hotkeys — active app-wide when the active slice is in a CW - // mode (CW or CWL). Guard prevents collisions with a future DVK - // macro panel or other function-key users. (#1552) + // ── F1-F12 hotkeys — active app-wide when the TX slice is in a CW + // mode (CW or CWL). CWX keys the TX slice, so the guard follows it, + // not the selected RX slice. Guard prevents collisions with the DVK + // macro panel or other function-key users. (#1552, #4173) // // Created disabled; MainWindow flips enable state based on the - // active slice's mode (mutually exclusive with DvkPanel's F1-F12 + // TX slice's mode (mutually exclusive with DvkPanel's F1-F12 // set) so the keys fire regardless of panel visibility, while // Qt still sees at most one enabled ApplicationShortcut per key // and never emits activatedAmbiguously. (#2464, #2582) @@ -300,8 +301,8 @@ CwxPanel::CwxPanel(CwxModel* model, QWidget* parent) m_shortcuts.append(sc); connect(sc, &QShortcut::activated, this, [this, i]() { if (!m_model) return; - if (m_activeModeProvider) { - const QString mode = m_activeModeProvider(); + if (m_txModeProvider) { + const QString mode = m_txModeProvider(); if (mode != QLatin1String("CW") && mode != QLatin1String("CWL")) return; } diff --git a/src/gui/CwxPanel.h b/src/gui/CwxPanel.h index 87ba9f43e..f3838a938 100644 --- a/src/gui/CwxPanel.h +++ b/src/gui/CwxPanel.h @@ -66,11 +66,13 @@ class CwxPanel : public QWidget { // Optional providers used to guard the global F1-F12 / ESC shortcuts // so they don't fire in modes/states where they'd be surprising (#1552). - // - modeProvider returns the active slice's mode ("CW", "CWL", ...) + // - txModeProvider returns the TX slice's mode ("CW", "CWL", ...) — CWX + // keys the TX slice, so the guard follows it, not the selected RX slice + // (matches indicator availability; #4173) // - transmittingProvider returns true when the radio is actively TXing // When unset, the shortcuts fire unconditionally (legacy behavior). - void setActiveModeProvider(std::function provider) { - m_activeModeProvider = std::move(provider); + void setTxModeProvider(std::function provider) { + m_txModeProvider = std::move(provider); } void setTransmittingProvider(std::function provider) { m_transmittingProvider = std::move(provider); @@ -139,7 +141,7 @@ private slots: QPushButton* m_setupBtn{nullptr}; QSpinBox* m_speedSpin{nullptr}; - std::function m_activeModeProvider; + std::function m_txModeProvider; std::function m_transmittingProvider; // F1-F12 + ESC shortcuts — enabled by MainWindow based on the active diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 4c2924070..bc478243a 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3924,9 +3924,11 @@ void MainWindow::buildUI() // CWX panel — left of spectrum, hidden by default m_cwxPanel = new CwxPanel(&m_radioModel.cwxModel(), splitter); // Provide state probes so CWX can guard its F1-F12 / ESC app-wide - // shortcuts on mode + transmit state (#1552). - m_cwxPanel->setActiveModeProvider([this]() { - auto* s = activeSlice(); + // shortcuts on the TX slice's mode + transmit state. CWX keys the TX + // slice, so the mode guard follows it, not the selected RX slice — matching + // the indicator's availability (#1552, #4173). + m_cwxPanel->setTxModeProvider([this]() { + auto* s = m_radioModel.txSlice(); return s ? s->mode() : QString(); }); m_cwxPanel->setTransmittingProvider([this]() { @@ -6243,8 +6245,8 @@ void MainWindow::setActiveSliceInternal(int sliceId, bool revealOffscreen) routeRttyDecoderOutput(); refreshRttyDecodeState(); - // Update CWX/DVK indicator availability for this slice's mode - updateKeyerAvailability(s->mode()); + // Update CWX/DVK indicator availability (follows the TX slice, #4173) + updateKeyerAvailability(); // Detect band from frequency if (m_bandSettings.currentBand().isEmpty()) @@ -7853,38 +7855,38 @@ void MainWindow::showPanadapterInterlockNotification(const QString& message, // ─── Keyboard Shortcuts ─────────────────────────────────────────────────────── -void MainWindow::updateKeyerAvailability(const QString& activeMode) +void MainWindow::updateKeyerAvailability() { static const QString kActive = "QLabel { color: #00b4d8; font-weight: bold; font-size: 24px; }"; static const QString kAvail = "QLabel { color: #404858; font-weight: bold; font-size: 24px; }"; static const QString kDisabled = "QLabel { color: #252530; font-weight: bold; font-size: 24px; }"; - const bool activeIsCw = (activeMode == "CW" || activeMode == "CWL"); - const bool activeIsSsb = (activeMode == "USB" || activeMode == "LSB" - || activeMode == "AM" || activeMode == "SAM" - || activeMode == "FM" || activeMode == "NFM" - || activeMode == "DFM"); - - QString txMode; - for (SliceModel* slice : m_radioModel.slices()) { - if (slice && slice->isTxSlice()) { - txMode = slice->mode(); - break; - } - } - const bool txIsCw = (txMode == "CW" || txMode == "CWL"); - - // F1-F12 / Esc ApplicationShortcuts: enable the set that matches the - // active slice's mode, regardless of panel visibility. The two sets - // are mutually exclusive so Qt never sees two enabled shortcuts for - // the same key and won't emit activatedAmbiguously (#2464, #2582). - if (m_cwxPanel) m_cwxPanel->setShortcutsEnabled(activeIsCw); - if (m_dvkPanel) m_dvkPanel->setShortcutsEnabled(activeIsSsb); - - // CWX sends on the radio's TX slice (FlexLib CWX::getTXFrequency), so - // availability follows that slice rather than the selected RX slice. + // CWX and DVK both key the radio's TX slice, so their availability and + // F1-F12 shortcuts follow that slice — not the selected RX slice. FlexLib + // scopes CWX to the TX slice (reference/FlexLib_API_v4.1.5.39794/FlexLib/ + // CWX.cs:186-199 getTXFrequency() returns the IsTransmitSlice's Freq). + // Driving both keyers from the *same* slice's mode keeps the two F1-F12 + // sets mutually exclusive (CW vs voice can't both be true), so Qt never + // sees two enabled ApplicationShortcuts per key and won't emit + // activatedAmbiguously (#2464, #2582, #4173). + SliceModel* txSlice = m_radioModel.txSlice(); + const QString txMode = txSlice ? txSlice->mode() : QString(); + const bool txIsCw = (txMode == "CW" || txMode == "CWL"); + const bool txIsSsb = (txMode == "USB" || txMode == "LSB" + || txMode == "AM" || txMode == "SAM" + || txMode == "FM" || txMode == "NFM" + || txMode == "DFM"); + + if (m_cwxPanel) m_cwxPanel->setShortcutsEnabled(txIsCw); + if (m_dvkPanel) m_dvkPanel->setShortcutsEnabled(txIsSsb); + + // Only auto-hide an open panel when a TX slice *exists* and is in the wrong + // mode (a deliberate mode change). A momentary "no TX slice" — during a TX + // handoff between slices, or a band-recall that drops+recreates the TX slice + // — must not yank an open panel the user can't get back (updateKeyer only + // hides; showing is user-driven) (#4173). m_cwxIndicator->setEnabled(txIsCw); - if (!txIsCw && m_cwxPanel->isVisible()) { + if (txSlice && !txIsCw && m_cwxPanel->isVisible()) { m_cwxPanel->hide(); m_cwxIndicator->setStyleSheet(kDisabled); } else if (m_cwxPanel->isVisible()) { @@ -7895,16 +7897,16 @@ void MainWindow::updateKeyerAvailability(const QString& activeMode) m_cwxIndicator->setCursor(txIsCw ? Qt::PointingHandCursor : Qt::ArrowCursor); // DVK: available in voice modes (SSB, AM, FM — not DIGU/DIGL) - m_dvkIndicator->setEnabled(activeIsSsb); - if (!activeIsSsb && m_dvkPanel->isVisible()) { + m_dvkIndicator->setEnabled(txIsSsb); + if (txSlice && !txIsSsb && m_dvkPanel->isVisible()) { m_dvkPanel->hide(); m_dvkIndicator->setStyleSheet(kDisabled); } else if (m_dvkPanel->isVisible()) { m_dvkIndicator->setStyleSheet(kActive); } else { - m_dvkIndicator->setStyleSheet(activeIsSsb ? kAvail : kDisabled); + m_dvkIndicator->setStyleSheet(txIsSsb ? kAvail : kDisabled); } - m_dvkIndicator->setCursor(activeIsSsb ? Qt::PointingHandCursor : Qt::ArrowCursor); + m_dvkIndicator->setCursor(txIsSsb ? Qt::PointingHandCursor : Qt::ArrowCursor); } void MainWindow::centerActiveSliceInPanadapter(bool forceRadioCenter, double centerMhz) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 966ac08d4..6bbcfcdfd 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -589,7 +589,7 @@ private slots: #ifdef HAVE_WEBSOCKETS void showFreeDvReporter(); #endif - void updateKeyerAvailability(const QString& activeMode); + void updateKeyerAvailability(); void showNr2ParamPopup(const QPoint& globalPos); void showNr4ParamPopup(const QPoint& globalPos); void showDfnrParamPopup(const QPoint& globalPos); diff --git a/src/gui/MainWindow_Shortcuts.cpp b/src/gui/MainWindow_Shortcuts.cpp index 0dad3c4ab..917600ce0 100644 --- a/src/gui/MainWindow_Shortcuts.cpp +++ b/src/gui/MainWindow_Shortcuts.cpp @@ -555,8 +555,7 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) // Close DVK (mutual exclusion) if (show && m_dvkPanel->isVisible()) { m_dvkPanel->hide(); - auto* sl = activeSlice(); - updateKeyerAvailability(sl ? sl->mode() : QString()); + updateKeyerAvailability(); } m_cwxPanel->setVisible(show); m_cwxIndicator->setStyleSheet(show @@ -581,8 +580,7 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event) // Close CWX (mutual exclusion) if (show && m_cwxPanel->isVisible()) { m_cwxPanel->hide(); - auto* sl = activeSlice(); - updateKeyerAvailability(sl ? sl->mode() : QString()); + updateKeyerAvailability(); } m_dvkPanel->setVisible(show); m_dvkIndicator->setStyleSheet(show diff --git a/src/gui/MainWindow_Wiring.cpp b/src/gui/MainWindow_Wiring.cpp index 57cf42e3b..167ae051f 100644 --- a/src/gui/MainWindow_Wiring.cpp +++ b/src/gui/MainWindow_Wiring.cpp @@ -1328,8 +1328,8 @@ void MainWindow::onSliceAdded(SliceModel* s) syncTxWaterfallSliceToSpectrums(); updateSplitState(); - SliceModel* active = activeSlice(); - updateKeyerAvailability(active ? active->mode() : QString()); + // TX flag just moved — keyer availability follows the TX slice (#4173). + updateKeyerAvailability(); // Active follows TX slice (#1351) — switch the displayed/active slice // when an external program (e.g. WSJT-X) moves the TX flag @@ -1384,12 +1384,10 @@ void MainWindow::onSliceAdded(SliceModel* s) } } - // Shortcuts follow the selected slice, while CWX availability follows - // the TX slice. Re-evaluate when either slice changes mode (#4173). - if (s->sliceId() == m_activeSliceId || s->isTxSlice()) { - SliceModel* active = activeSlice(); - updateKeyerAvailability(active ? active->mode() : QString()); - } + // CWX/DVK availability and their F1-F12 shortcuts follow the TX slice, + // so re-evaluate only when the TX slice changes mode (#4173). + if (s->isTxSlice()) + updateKeyerAvailability(); #ifdef HAVE_RADE if (mode.startsWith("FDV")) activateFdvDisplay(s->sliceId()); diff --git a/src/models/RadioModel.h b/src/models/RadioModel.h index 4e0a3abb6..2f4195f6f 100644 --- a/src/models/RadioModel.h +++ b/src/models/RadioModel.h @@ -372,6 +372,10 @@ class RadioModel : public QObject { QList slices() const { return m_slices; } SliceModel* slice(int id) const; + // The slice that transmits (IsTransmitSlice), or nullptr if none. Mirrors + // FlexLib's TX-slice scan (CWX::getTXFrequency, CWX.cs:186) — keyer/CWX + // targeting follows this slice, not the selected RX slice. + SliceModel* txSlice() const; QMap rawSliceModeLists() const { return m_rawSliceModeLists; } int rawModeOccurrenceCount(const QString& mode) const; int activeTxSliceNum() const; @@ -930,7 +934,6 @@ private slots: int bandIdForFrequency(double freqMhz) const; // map TX freq → band ID void applyTuneInhibit(); // suppress selected TX outputs before tune void restoreTuneInhibit(); // re-enable TX outputs after tune - SliceModel* txSlice() const; QString transmitInhibitMessageForSlice(const SliceModel* slice) const; QString transmitInhibitMessageForTxSlice() const; void enforceTransmitInhibitForPan(const QString& panId);