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 2f12ceb41..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,46 +7855,58 @@ void MainWindow::showPanadapterInterlockNotification(const QString& message, // ─── Keyboard Shortcuts ─────────────────────────────────────────────────────── -void MainWindow::updateKeyerAvailability(const QString& mode) +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; }"; - bool isCw = (mode == "CW" || mode == "CWL"); - bool isSsb = (mode == "USB" || mode == "LSB" || mode == "AM" || mode == "SAM" - || mode == "FM" || mode == "NFM" || mode == "DFM"); - - // 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); - - // CWX: available in CW modes only - m_cwxIndicator->setEnabled(isCw); - if (!isCw && m_cwxPanel->isVisible()) { + // 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 (txSlice && !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(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(isSsb ? kAvail : kDisabled); + m_dvkIndicator->setStyleSheet(txIsSsb ? kAvail : kDisabled); } - m_dvkIndicator->setCursor(isSsb ? 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 48e8a5d73..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& mode); + 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 a2f3ca7c6..167ae051f 100644 --- a/src/gui/MainWindow_Wiring.cpp +++ b/src/gui/MainWindow_Wiring.cpp @@ -1328,6 +1328,9 @@ void MainWindow::onSliceAdded(SliceModel* s) syncTxWaterfallSliceToSpectrums(); updateSplitState(); + // 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 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,11 @@ void MainWindow::onSliceAdded(SliceModel* s) QMetaObject::invokeMethod(m_audio, [this]() { m_audio->setNvAfxEnabled(false); }); } } + + // 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);