Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/gui/CwxPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
Expand Down
10 changes: 6 additions & 4 deletions src/gui/CwxPanel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QString()> provider) {
m_activeModeProvider = std::move(provider);
void setTxModeProvider(std::function<QString()> provider) {
m_txModeProvider = std::move(provider);
}
void setTransmittingProvider(std::function<bool()> provider) {
m_transmittingProvider = std::move(provider);
Expand Down Expand Up @@ -139,7 +141,7 @@ private slots:
QPushButton* m_setupBtn{nullptr};
QSpinBox* m_speedSpin{nullptr};

std::function<QString()> m_activeModeProvider;
std::function<QString()> m_txModeProvider;
std::function<bool()> m_transmittingProvider;

// F1-F12 + ESC shortcuts — enabled by MainWindow based on the active
Expand Down
66 changes: 40 additions & 26 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]() {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/gui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 2 additions & 4 deletions src/gui/MainWindow_Shortcuts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 8 additions & 3 deletions src/gui/MainWindow_Wiring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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());
Expand Down
5 changes: 4 additions & 1 deletion src/models/RadioModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ class RadioModel : public QObject {

QList<SliceModel*> 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<int, QString> rawSliceModeLists() const { return m_rawSliceModeLists; }
int rawModeOccurrenceCount(const QString& mode) const;
int activeTxSliceNum() const;
Expand Down Expand Up @@ -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);
Expand Down
Loading