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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,7 @@ set(GUI_SOURCES
src/gui/GreenHeronApplet.cpp
src/gui/TitleBar.cpp
src/gui/RoundedMenu.cpp
src/gui/StatusIndicator.cpp
src/gui/BrandMark.cpp
src/gui/RadioTabBar.cpp
src/gui/WindowCaptionButtons.cpp
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/aetherd-touchpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine
| `core/SystemInventory.h` | 1 | ui-support — Startup and support-bundle inventory of host CPU, SIMD and RAM capabilities. Process diagnostics, not radio state. | unconverted |
| `core/TciServer.h` | 3 | mixed(flex) — TCI WebSocket server for WSJT-X et al: protocol surface is canonical radio state, but audio/IQ rides Flex DAX | unconverted |
| `core/TgxlConnection.h` | 3 | peripheral(4o3a) — Direct TCP client for the 4O3A Tuner Genius XL (port 9010, relay/autotune), reverse-engineered from the 4O3A management app — a standalone accessory transport, not SmartSDR. Not radio-family wire; a peripheral accessory, NOT behind the IRadioBackend radio seam (reclassified from vendor(flex), #4087 follow-up). | unconverted |
| `core/ThemeManager.h` | 167 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
| `core/ThemeManager.h` | 168 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted |
| `core/ThreadCpuRing.h` | 2 | ui-support — Short host-thread CPU history used by Runtime Monitor peak and sparkline presentation. Diagnostic UI support, not radio state. | unconverted |
| `core/TimeFrameVoter.h` | 1 | universal — Shared AetherClock time-frame types plus confidence-weighted cross-frame bit voting over a sliding window. Map-agnostic pure DSP/logic — no Qt, no GUI, no vendor ties. | unconverted |
| `core/TxKeyingMarker.h` | 15 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted |
Expand Down
33 changes: 33 additions & 0 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
#include "DvkAvailabilityGate.h"
#include "VoiceModeGate.h"
#include "DvkPanel.h"
#include "StatusIndicator.h"
#include "core/DvkWavTransfer.h"
#include "AmpApplet.h"
#include "MeterApplet.h"
Expand Down Expand Up @@ -5656,6 +5657,15 @@ void MainWindow::buildUI()
const QString redInd = "QLabel { color: #e04040; font-weight: bold; font-size: 21px; }";
const QString greyIndLg = "QLabel { color: #404858; font-weight: bold; font-size: 24px; }";
const QString greenIndLg= "QLabel { color: #00e060; font-weight: bold; font-size: 24px; }";
// Tab, Return/Enter and the accessible Press action reach the same action
// as a click (#6257, docs/a11y.md interactive-QLabel rule). On/off
// indicators are checkable, so their state is not colour alone.
const auto keyboardOperable = [this](QWidget* indicator, bool onOff = false) {
StatusIndicator* helper = StatusIndicator::attach(indicator);
helper->setCheckable(onOff);
connect(helper, &StatusIndicator::activated, this,
[this, indicator] { activateStatusIndicator(indicator); });
};

// Use a container with HBoxLayout for 3-section layout:
// [left items] → stretch → [STATION centered] → stretch → [right items]
Expand Down Expand Up @@ -5733,7 +5743,9 @@ void MainWindow::buildUI()
m_bandStackIndicator->setPixmap(buildBandStackIndicatorPixmap(false));
m_bandStackIndicator->setCursor(Qt::PointingHandCursor);
m_bandStackIndicator->setToolTip("Open band stack panel");
m_bandStackIndicator->setAccessibleName(QStringLiteral("Band stack panel"));
m_bandStackIndicator->installEventFilter(this);
keyboardOperable(m_bandStackIndicator, true);
hbox->addWidget(m_bandStackIndicator);
}

Expand All @@ -5746,6 +5758,7 @@ void MainWindow::buildUI()
addPanBtn->setCursor(Qt::PointingHandCursor);
addPanBtn->setToolTip("Add Panadapter");
addPanBtn->installEventFilter(this);
keyboardOperable(addPanBtn);
hbox->addWidget(addPanBtn);
m_addPanLabel = addPanBtn;
}
Expand All @@ -5756,7 +5769,9 @@ void MainWindow::buildUI()
m_tnfIndicator->setStyleSheet(greyIndLg);
m_tnfIndicator->setCursor(Qt::PointingHandCursor);
m_tnfIndicator->setToolTip(buildTnfTooltip(m_radioModel.tnfModel()));
m_tnfIndicator->setAccessibleName(QStringLiteral("Tracking notch filters"));
m_tnfIndicator->installEventFilter(this);
keyboardOperable(m_tnfIndicator, true);
hbox->addWidget(m_tnfIndicator);
auto updateTnfTooltip = [this]() {
if (m_tnfIndicator) {
Expand All @@ -5772,15 +5787,19 @@ void MainWindow::buildUI()
m_cwxIndicator->setStyleSheet(greyIndLg);
m_cwxIndicator->setCursor(Qt::PointingHandCursor);
m_cwxIndicator->setToolTip("CW Keyer — click to toggle");
m_cwxIndicator->setAccessibleName(QStringLiteral("CW keyer"));
m_cwxIndicator->installEventFilter(this);
keyboardOperable(m_cwxIndicator, true);
hbox->addWidget(m_cwxIndicator);

#ifdef AETHER_ASR_ENABLED
m_asrIndicator = new QLabel("ASR");
m_asrIndicator->setStyleSheet(greyIndLg);
m_asrIndicator->setCursor(Qt::PointingHandCursor);
m_asrIndicator->setToolTip("Speech-to-text (Copy Assist) — click to toggle");
m_asrIndicator->setAccessibleName(QStringLiteral("Speech-to-text (Copy Assist)"));
m_asrIndicator->installEventFilter(this);
keyboardOperable(m_asrIndicator, true);
hbox->addWidget(m_asrIndicator);
#endif

Expand All @@ -5792,13 +5811,16 @@ void MainWindow::buildUI()
m_dvkIndicator->setToolTip(dvkIndicatorTooltip(DvkIndicatorBlocker::None));
m_dvkIndicator->setAccessibleDescription(dvkIndicatorTooltip(DvkIndicatorBlocker::None));
m_dvkIndicator->installEventFilter(this);
keyboardOperable(m_dvkIndicator, true);
hbox->addWidget(m_dvkIndicator);

m_fdxIndicator = new QLabel("FDX");
m_fdxIndicator->setStyleSheet(greyIndLg);
m_fdxIndicator->setCursor(Qt::PointingHandCursor);
m_fdxIndicator->setToolTip("Full Duplex — RX stays active during TX (click to toggle)");
m_fdxIndicator->setAccessibleName(QStringLiteral("Full duplex"));
m_fdxIndicator->installEventFilter(this);
keyboardOperable(m_fdxIndicator, true);
hbox->addWidget(m_fdxIndicator);

addSep();
Expand Down Expand Up @@ -6111,7 +6133,10 @@ void MainWindow::buildUI()
m_tgxlContainer->setToolTip("Tuner Genius XL\nClick to cycle OPERATE / BYPASS / STANDBY");
m_tgxlContainer->setAccessibleName("Tuner Genius XL status");
m_tgxlContainer->setAccessibleDescription("Click to cycle between OPERATE, BYPASS, and STANDBY");
m_tgxlContainer->setProperty("aetherStateCycle",
QStringLiteral("Press to cycle between OPERATE, BYPASS, and STANDBY"));
m_tgxlContainer->installEventFilter(this);
keyboardOperable(m_tgxlContainer);
m_tgxlContainer->setVisible(false);
{
auto* vbox = new QVBoxLayout(m_tgxlContainer);
Expand Down Expand Up @@ -6148,7 +6173,10 @@ void MainWindow::buildUI()
m_pgxlContainer->setToolTip("Power Genius XL\nClick to cycle OPERATE / STANDBY");
m_pgxlContainer->setAccessibleName("Power Genius XL status");
m_pgxlContainer->setAccessibleDescription("Click to cycle between OPERATE and STANDBY");
m_pgxlContainer->setProperty("aetherStateCycle",
QStringLiteral("Press to cycle between OPERATE and STANDBY"));
m_pgxlContainer->installEventFilter(this);
keyboardOperable(m_pgxlContainer);
m_pgxlContainer->setVisible(false);
{
auto* vbox = new QVBoxLayout(m_pgxlContainer);
Expand Down Expand Up @@ -6183,6 +6211,7 @@ void MainWindow::buildUI()
m_txIndicator->setAccessibleName("Cancel transmit");
m_txIndicator->setAccessibleDescription("Click to send key up, PTT off, Tune off, and MOX off.");
m_txIndicator->installEventFilter(this);
keyboardOperable(m_txIndicator);
m_txIndicator->setStyleSheet("QLabel { color: rgba(255,255,255,128); font-weight: bold; font-size: 21px; }");
hbox->addWidget(m_txIndicator);

Expand Down Expand Up @@ -7356,6 +7385,7 @@ void MainWindow::updateBandStackIndicator()
m_bandStackIndicator->setPixmap(buildBandStackIndicatorPixmap(visible));
m_bandStackIndicator->setToolTip(visible ? "Close band stack panel"
: "Open band stack panel");
StatusIndicator::setCheckedFor(m_bandStackIndicator, visible);
}

bool MainWindow::activateMemorySpot(int memoryIndex, const QString& preferredPanId)
Expand Down Expand Up @@ -10461,6 +10491,7 @@ void MainWindow::updateKeyerAvailability()
setIndicatorStyle(m_cwxIndicator, txIsCw ? kAvail : kDisabled);
}
m_cwxIndicator->setCursor(txIsCw ? Qt::PointingHandCursor : Qt::ArrowCursor);
StatusIndicator::setCheckedFor(m_cwxIndicator, m_cwxPanel->isVisible());

// DVK: available in voice modes (SSB, AM, FM — not DIGU/DIGL), and only on
// a radio that reports the DVK entitlement (see dvkIndicatorBlocker).
Expand All @@ -10478,6 +10509,7 @@ void MainWindow::updateKeyerAvailability()
}
m_dvkIndicator->setCursor(dvkAvailable ? Qt::PointingHandCursor
: Qt::ArrowCursor);
StatusIndicator::setCheckedFor(m_dvkIndicator, m_dvkPanel->isVisible());
// An unlicensed radio gets a tooltip naming the missing subscription; the
// mode gate keeps the normal one, since the panel's own title and the F-key
// rows already make "wrong mode" obvious once it opens.
Expand Down Expand Up @@ -10541,6 +10573,7 @@ void MainWindow::updateKeyerAvailability()
setIndicatorStyle(m_asrIndicator,
asrVisible ? kActive
: (asrIsVoice ? kAvail : kDisabled));
StatusIndicator::setCheckedFor(m_asrIndicator, asrVisible);
// Cursor tracks the ENABLED state, not the mode, so the hand appears on
// exactly the clicks that do something — including the close-an-open-
// panel case where the mode gate says no.
Expand Down
4 changes: 4 additions & 0 deletions src/gui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,10 @@ private slots:
// keyer panels mutually exclusive and restore the splitter identically.
void toggleCwKeyerPanel();
void toggleVoiceKeyerPanel();
// Status-bar indicators are click-handled labels and containers; a mouse
// press (eventFilter) and the keyboard (StatusIndicator) share one action.
bool isStatusIndicator(const QObject* obj) const;
void activateStatusIndicator(QObject* obj);
// Shared by the status-bar +PAN affordance and Tools ▸ Add Panadapter… so
// both route through PanLayoutDialog and the layout machinery.
void showAddPanadapterDialog();
Expand Down
82 changes: 55 additions & 27 deletions src/gui/MainWindow_Shortcuts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -851,24 +851,55 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event)
toggleConnectionDialog();
return true;
}
// Status-bar indicators: a press and the keyboard (StatusIndicator's
// Return/Enter/Space and the accessible Press action) run one action.
if (event->type() == QEvent::MouseButtonPress && isStatusIndicator(obj)) {
if (obj == m_txIndicator
&& static_cast<QMouseEvent*>(event)->button() != Qt::LeftButton) {
return true;
}
activateStatusIndicator(obj);
return true;
}
return QMainWindow::eventFilter(obj, event);
}

bool MainWindow::isStatusIndicator(const QObject* obj) const
{
if (!obj) {
return false;
}
#ifdef AETHER_ASR_ENABLED
if (obj == m_asrIndicator && event->type() == QEvent::MouseButtonPress) {
if (!m_asrIndicator->isEnabled()) return true;
if (obj == m_asrIndicator) {
return true;
}
#endif
return obj == m_cwxIndicator || obj == m_tnfIndicator
|| obj == m_fdxIndicator || obj == m_bandStackIndicator || obj == m_tgxlContainer
|| obj == m_pgxlContainer || obj == m_txIndicator || obj == m_addPanLabel
|| obj == m_dvkIndicator;
}

void MainWindow::activateStatusIndicator(QObject* obj)
{
#ifdef AETHER_ASR_ENABLED
if (obj == m_asrIndicator) {
if (!m_asrIndicator->isEnabled()) return;
showCopyAssist(); // toggles the docked Copy Assist panel
updateKeyerAvailability(); // refresh the indicator's active/available style
return true;
return;
}
#endif
if (obj == m_cwxIndicator && event->type() == QEvent::MouseButtonPress) {
if (!m_cwxIndicator->isEnabled()) return true;
if (obj == m_cwxIndicator) {
if (!m_cwxIndicator->isEnabled()) return;
toggleCwKeyerPanel();
return true;
return;
}
if (obj == m_tnfIndicator && event->type() == QEvent::MouseButtonPress) {
if (obj == m_tnfIndicator) {
m_radioModel.tnfModel().requestGlobalTnfEnabled(!m_radioModel.tnfModel().globalEnabled());
return true;
return;
}
if (obj == m_fdxIndicator && event->type() == QEvent::MouseButtonPress) {
if (obj == m_fdxIndicator) {
bool on = !m_radioModel.fullDuplexEnabled();
m_radioModel.sendCmdPublic(
QString("radio set full_duplex_enabled=%1").arg(on ? 1 : 0),
Expand All @@ -882,15 +913,15 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event)
// Radio accepted; no status echo follows, so apply manually.
m_radioModel.setFullDuplex(on);
});
return true;
return;
}
if (obj == m_bandStackIndicator && event->type() == QEvent::MouseButtonPress) {
if (obj == m_bandStackIndicator) {
bool show = !m_panStack->bandStackPanel()->isVisible();
setBandStackPanelVisible(show);
updateBandStackIndicator();
return true;
return;
}
if (obj == m_tgxlContainer && event->type() == QEvent::MouseButtonPress) {
if (obj == m_tgxlContainer) {
auto& t = m_radioModel.tunerModel();
// Cycle: OPERATE → BYPASS → STANDBY → OPERATE
if (t.isOperate() && !t.isBypass())
Expand All @@ -901,29 +932,26 @@ bool MainWindow::eventFilter(QObject* obj, QEvent* event)
t.setBypass(false);
t.setOperate(true);
}
return true;
return;
}
if (obj == m_pgxlContainer && event->type() == QEvent::MouseButtonPress) {
if (obj == m_pgxlContainer) {
// Simple toggle: OPERATE ↔ STANDBY (PGXL has no BYPASS)
m_radioModel.amplifier().setOperate(!m_radioModel.amplifier().operate());
return true;
return;
}
if (obj == m_txIndicator && event->type() == QEvent::MouseButtonPress) {
auto* mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton)
cancelTransmitFromIndicator();
return true;
if (obj == m_txIndicator) {
cancelTransmitFromIndicator();
return;
}
if (obj == m_addPanLabel && event->type() == QEvent::MouseButtonPress) {
if (obj == m_addPanLabel) {
showAddPanadapterDialog();
return true;
return;
}
if (obj == m_dvkIndicator && event->type() == QEvent::MouseButtonPress) {
if (!m_dvkIndicator->isEnabled()) return true;
if (obj == m_dvkIndicator) {
if (!m_dvkIndicator->isEnabled()) return;
toggleVoiceKeyerPanel();
return true;
return;
}
return QMainWindow::eventFilter(obj, event);
}

// Shared by the status-bar +PAN affordance and Tools ▸ Add Panadapter… so both
Expand Down
13 changes: 13 additions & 0 deletions src/gui/MainWindow_Wiring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// panFollowVfo()/revealFrequencyIfNeeded().

#include "MainWindow.h"
#include "StatusIndicator.h"
#include "models/CwDecodeSettings.h"
#include "PeripheralAuthStore.h"
#include "core/backends/AutoRfGainControl.h"
Expand Down Expand Up @@ -4373,6 +4374,7 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
m_tnfIndicator->setStyleSheet(on
? "QLabel { color: #00b4d8; font-weight: bold; font-size: 24px; }"
: "QLabel { color: #404858; font-weight: bold; font-size: 24px; }");
StatusIndicator::setCheckedFor(m_tnfIndicator, on);
});

// FDX indicator style update
Expand All @@ -4383,6 +4385,7 @@ void MainWindow::wirePanadapter(PanadapterApplet* applet)
m_fdxIndicator->setStyleSheet(fdx
? "QLabel { color: #00b4d8; font-weight: bold; font-size: 24px; }"
: "QLabel { color: #404858; font-weight: bold; font-size: 24px; }");
StatusIndicator::setCheckedFor(m_fdxIndicator, fdx);
});
connect(sw, &SpectrumWidget::tnfCreateRequested, tnf, &TnfModel::createTnf,
Qt::UniqueConnection);
Expand Down Expand Up @@ -7305,6 +7308,16 @@ void MainWindow::wireMeters()
stateLbl->setStyleSheet(
QString("QLabel { color: %1; font-size:11px; }").arg(color));
stateLbl->setText(state);
// The TUN/AMP container is the keyboard target; its description leads
// with the state, which the colour alone otherwise carries.
if (QWidget* container = stateLbl->parentWidget()) {
const QString cycle = container->property("aetherStateCycle").toString();
const QString described = cycle.isEmpty() ? state
: QStringLiteral("%1. %2").arg(state, cycle);
if (container->accessibleDescription() != described) {
container->setAccessibleDescription(described);
}
}
};

auto updateTgxlStyle = [this, setIndicatorHtml]() {
Expand Down
Loading
Loading