Skip to content

Commit b48c3aa

Browse files
committed
refactor(gui): extract pure helpers from MainWindow.cpp to MainWindowHelpers (#3351 Phase 0)
First PR of the #3351 monolith-decomposition series. Pure code motion — no behavior change. Moves the ~20 file-scope static helper functions that have no MainWindow dependency (no members, no mutable file-scope state) out of MainWindow.cpp into a new MainWindowHelpers.{h,cpp}: - Network diagnostics tooltip: formatNetworkMs, formatNetworkSeqErrors, buildNetworkTooltip - TNF tooltip: tnfFrequencyHz, formatTnfFrequency, formatTnfDepth, buildTnfTooltip - Memory/passive spot-ID math: memorySpotId, memoryIndexFromSpotId, isPassiveLocalSpotId, memorySpotLabel, memorySpotComment (plus the kMemorySpotIdBase / kPassiveSpotIdBase constants, used only by these) - Client connection parsing: splitClientField, parseClientHandle, buildDisconnectClients ×3, cleanClientDisplayText, clientConnectionStatusMessage - Misc: buildBandStackIndicatorPixmap, shortcutSequenceFromKeyEvent, macDaxDriverInstalled Deliberately left behind: the shortcut-lease helpers (textInputCaptured, shortcutGuard, leaseHolderBusy, etc.) — they read the mutable file-scope statics s_keyboardShortcutsEnabled / s_sliderShortcutLeaseActive and are destined for the Shortcuts TU in Phase 1. The header documents the rule: state-coupled helpers don't belong in this file. Two incidental notes: - The functions lose internal linkage (static → namespace-scope external). No ODR risk: names are project-prefixed contextually and live in namespace AetherSDR. - The long-standing Linux "-Wunused-function: macDaxDriverInstalled" warning at the tail of every build disappears — the function's only caller is inside #ifdef Q_OS_MAC, and external-linkage functions don't trigger that warning. One transplant fix: the CategoryStats convenience overload of formatNetworkSeqErrors became formatCategorySeqErrors (anon-namespace, private to the new TU) — as an anonymous-namespace *overload* it would have hidden the namespace-scope two-int overload from unqualified lookup. MainWindow.cpp: 19,474 → 19,138 lines. Verified: full build clean, all 32 ctest suites pass. Refs #3351. Principle XI.
1 parent ce2437a commit b48c3aa

4 files changed

Lines changed: 475 additions & 341 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ set(MODEL_SOURCES
634634

635635
set(GUI_SOURCES
636636
src/gui/MainWindow.cpp
637+
src/gui/MainWindowHelpers.cpp
637638
src/gui/AgcCalibrationDialog.cpp
638639
src/gui/AudioDeviceChangeDialog.cpp
639640
src/gui/ConnectionPanel.cpp

src/gui/MainWindow.cpp

Lines changed: 5 additions & 341 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "MainWindow.h"
22

3+
#include "MainWindowHelpers.h"
4+
35
#include "CwDecodeSettings.h"
46
#include "DisplaySettings.h"
57
#ifdef HAVE_MQTT
@@ -635,186 +637,13 @@ double quantizeIncrementalFollowDelta(double overshootMhz, double stepMhz)
635637

636638
} // namespace
637639

638-
static bool macDaxDriverInstalled()
639-
{
640-
#ifdef Q_OS_MAC
641-
const QFileInfo driverBundle("/Library/Audio/Plug-Ins/HAL/AetherSDRDAX.driver");
642-
if (!driverBundle.exists() || !driverBundle.isDir())
643-
return false;
644-
645-
const QString bundlePath = driverBundle.absoluteFilePath();
646-
const QFileInfo driverExec(bundlePath + "/Contents/MacOS/AetherSDRDAX");
647-
const QFileInfo infoPlist(bundlePath + "/Contents/Info.plist");
648-
return driverExec.exists() && driverExec.isFile() && infoPlist.exists() && infoPlist.isFile();
649-
#else
650-
return true;
651-
#endif
652-
}
653-
654-
static QString formatNetworkMs(int ms)
655-
{
656-
return ms < 1 ? "< 1 ms" : QString("%1 ms").arg(ms);
657-
}
658-
659-
static QString formatNetworkSeqErrors(int errors, int packets)
660-
{
661-
if (packets == 0) {
662-
return "0 / 0 packets";
663-
}
664-
665-
const double pct = (errors * 100.0) / packets;
666-
return QString("%1 / %2 packets (%3%)")
667-
.arg(errors)
668-
.arg(packets)
669-
.arg(pct, 0, 'f', 2);
670-
}
671-
672-
static QString formatNetworkSeqErrors(const PanadapterStream::CategoryStats& stats)
673-
{
674-
return formatNetworkSeqErrors(stats.errors, stats.packets);
675-
}
676-
677-
static QString buildNetworkTooltip(const RadioModel& model)
678-
{
679-
const PanadapterStream::CategoryStats audioStats =
680-
model.categoryStats(PanadapterStream::CatAudio);
681-
const PanadapterStream::CategoryStats fftStats =
682-
model.categoryStats(PanadapterStream::CatFFT);
683-
const PanadapterStream::CategoryStats waterfallStats =
684-
model.categoryStats(PanadapterStream::CatWaterfall);
685-
const PanadapterStream::CategoryStats meterStats =
686-
model.categoryStats(PanadapterStream::CatMeter);
687-
const PanadapterStream::CategoryStats daxStats =
688-
model.categoryStats(PanadapterStream::CatDAX);
689-
690-
QStringList lines;
691-
lines
692-
<< QString("Network: %1").arg(model.networkQuality())
693-
<< QString("Latency (RTT): %1").arg(formatNetworkMs(model.lastPingRtt()))
694-
<< QString("Max RTT (session): %1").arg(formatNetworkMs(model.maxPingRtt()))
695-
<< QString("Packet loss (%1s): %2")
696-
.arg(model.packetLossWindowSeconds())
697-
.arg(formatNetworkSeqErrors(model.packetLossWindowDrops(),
698-
model.packetLossWindowPackets()))
699-
<< QString("Network jitter: %1").arg(formatNetworkMs(model.audioPacketJitterMs()))
700-
<< QString("Audio gap: %1 (max %2)")
701-
.arg(formatNetworkMs(model.audioPacketGapMs()),
702-
formatNetworkMs(model.audioPacketGapMaxMs()))
703-
<< QString("Total sequence gaps: %1")
704-
.arg(formatNetworkSeqErrors(model.packetDropCount(), model.packetTotalCount()))
705-
<< QString("Audio: %1").arg(formatNetworkSeqErrors(audioStats))
706-
<< QString("FFT: %1").arg(formatNetworkSeqErrors(fftStats))
707-
<< QString("Waterfall: %1").arg(formatNetworkSeqErrors(waterfallStats))
708-
<< QString("Meters: %1").arg(formatNetworkSeqErrors(meterStats))
709-
<< QString("DAX: %1").arg(formatNetworkSeqErrors(daxStats))
710-
<< QString("UDP RX bytes: %1").arg(QLocale().formattedDataSize(model.rxBytes()))
711-
<< QString("UDP TX bytes: %1").arg(QLocale().formattedDataSize(model.txBytes()))
712-
<< "Double-click for full diagnostics";
713-
return lines.join('\n');
714-
}
715-
716-
static long long tnfFrequencyHz(double freqMhz)
717-
{
718-
return static_cast<long long>(std::llround(freqMhz * 1.0e6));
719-
}
720-
721-
static QString formatTnfFrequency(double freqMhz)
722-
{
723-
const long long hz = tnfFrequencyHz(freqMhz);
724-
const int mhzPart = static_cast<int>(hz / 1000000);
725-
const int khzPart = static_cast<int>((hz / 1000) % 1000);
726-
const int hzPart = static_cast<int>(hz % 1000);
727-
return QStringLiteral("%1.%2.%3")
728-
.arg(mhzPart)
729-
.arg(khzPart, 3, 10, QChar('0'))
730-
.arg(hzPart, 3, 10, QChar('0'));
731-
}
732-
733-
static QString formatTnfDepth(int depthDb)
734-
{
735-
switch (std::clamp(depthDb, 1, 3)) {
736-
case 1:
737-
return QStringLiteral("Normal");
738-
case 2:
739-
return QStringLiteral("Deep");
740-
case 3:
741-
return QStringLiteral("Very Deep");
742-
default:
743-
return QStringLiteral("Normal");
744-
}
745-
}
746-
747-
static QString buildTnfTooltip(const TnfModel& tnfModel)
748-
{
749-
QString html = QStringLiteral(
750-
"<html><body style='white-space:nowrap;'>"
751-
"<div style='font-size:10pt; font-weight:600; color:#c8d8e8; margin-bottom:5px;'>"
752-
"Tracking Notch Filters — click to toggle"
753-
"</div>");
754-
755-
if (tnfModel.tnfs().isEmpty()) {
756-
html += QStringLiteral(
757-
"<div style='color:#8aa8c0;'>No TNF filters exist.</div>"
758-
"</body></html>");
759-
return html;
760-
}
761-
762-
QVector<TnfEntry> filters;
763-
filters.reserve(tnfModel.tnfs().size());
764-
for (const TnfEntry& tnf : tnfModel.tnfs()) {
765-
filters.append(tnf);
766-
}
767-
std::sort(filters.begin(), filters.end(), [](const TnfEntry& lhs, const TnfEntry& rhs) {
768-
const long long lhsHz = tnfFrequencyHz(lhs.freqMhz);
769-
const long long rhsHz = tnfFrequencyHz(rhs.freqMhz);
770-
if (lhsHz != rhsHz) {
771-
return lhsHz < rhsHz;
772-
}
773-
return lhs.id < rhs.id;
774-
});
775-
776-
html += QStringLiteral(
777-
"<table cellspacing='0' cellpadding='3'>"
778-
"<tr style='color:#8aa8c0; font-size:8pt;'>"
779-
"<th align='left'>Band</th>"
780-
"<th align='left'>Frequency</th>"
781-
"<th align='right'>Width</th>"
782-
"<th align='left'>Depth</th>"
783-
"<th align='left'>State</th>"
784-
"</tr>");
785-
786-
for (const TnfEntry& tnf : filters) {
787-
const QString band = BandSettings::bandForFrequency(tnf.freqMhz).toHtmlEscaped();
788-
const QString frequency = formatTnfFrequency(tnf.freqMhz).toHtmlEscaped();
789-
const QString width = QStringLiteral("%1 Hz").arg(tnf.widthHz).toHtmlEscaped();
790-
const QString depth = formatTnfDepth(tnf.depthDb).toHtmlEscaped();
791-
const QString state = tnf.permanent
792-
? QStringLiteral("Persistent")
793-
: QStringLiteral("Temporary");
794-
const QString stateColor = tnf.permanent
795-
? QStringLiteral("#30c030")
796-
: QStringLiteral("#ffc000");
797-
798-
html += QStringLiteral(
799-
"<tr>"
800-
"<td style='color:#c8d8e8;'>%1</td>"
801-
"<td style='color:#c8d8e8;'>%2 MHz</td>"
802-
"<td align='right' style='color:#c8d8e8;'>%3</td>"
803-
"<td style='color:#c8d8e8;'>%4</td>"
804-
"<td style='color:%5;'>&#9679; %6</td>"
805-
"</tr>")
806-
.arg(band, frequency, width, depth, stateColor, state);
807-
}
808-
809-
html += QStringLiteral("</table></body></html>");
810-
return html;
811-
}
640+
// Pure formatting / parsing helpers formerly defined here as file-scope
641+
// statics now live in MainWindowHelpers.{h,cpp} (#3351 Phase 0). Only
642+
// helpers coupled to the mutable shortcut-lease state below remain.
812643

813644
// ─── Shortcut guard (file-scope for use as std::function<bool()>) ───────────
814645

815646
static constexpr const char* kPaTempUnitSettingKey = "PaTempDisplayUnit";
816-
static constexpr int kMemorySpotIdBase = 1000000;
817-
static constexpr int kPassiveSpotIdBase = 2000000;
818647
static constexpr const char* kCwStraightKeyActionId = "cwkey";
819648
static constexpr const char* kCwLeftPaddleActionId = "cwdit";
820649
static constexpr const char* kCwRightPaddleActionId = "cwdah";
@@ -832,64 +661,6 @@ static bool isCwMomentaryActionId(const QString& id)
832661
|| id == QLatin1String(kCwRightPaddleActionId);
833662
}
834663

835-
static int memorySpotId(int memoryIndex)
836-
{
837-
return -(kMemorySpotIdBase + memoryIndex);
838-
}
839-
840-
static int memoryIndexFromSpotId(int spotIndex)
841-
{
842-
if (spotIndex > -kMemorySpotIdBase)
843-
return -1;
844-
return -spotIndex - kMemorySpotIdBase;
845-
}
846-
847-
static bool isPassiveLocalSpotId(int spotIndex)
848-
{
849-
return spotIndex <= -kPassiveSpotIdBase;
850-
}
851-
852-
static QString memorySpotLabel(const MemoryEntry& memory)
853-
{
854-
if (!memory.name.trimmed().isEmpty())
855-
return memory.name.trimmed();
856-
if (!memory.group.trimmed().isEmpty())
857-
return memory.group.trimmed();
858-
return QString("Memory %1").arg(memory.index);
859-
}
860-
861-
static QString memorySpotComment(const MemoryEntry& memory)
862-
{
863-
QStringList parts;
864-
if (!memory.group.trimmed().isEmpty())
865-
parts << QString("Group: %1").arg(memory.group.trimmed());
866-
if (!memory.owner.trimmed().isEmpty())
867-
parts << QString("Owner: %1").arg(memory.owner.trimmed());
868-
if (!memory.mode.trimmed().isEmpty())
869-
parts << QString("Mode: %1").arg(memory.mode.trimmed());
870-
if (memory.rxFilterLow != 0 || memory.rxFilterHigh != 0) {
871-
parts << QString("Filter: %1..%2 Hz")
872-
.arg(memory.rxFilterLow)
873-
.arg(memory.rxFilterHigh);
874-
}
875-
return parts.join(" | ");
876-
}
877-
878-
static QPixmap buildBandStackIndicatorPixmap(bool active)
879-
{
880-
QPixmap pixmap(10, 22);
881-
pixmap.fill(Qt::transparent);
882-
883-
QPainter painter(&pixmap);
884-
painter.setRenderHint(QPainter::Antialiasing);
885-
painter.setPen(Qt::NoPen);
886-
painter.setBrush(active ? QColor(0x00, 0xb4, 0xd8) : QColor(0x40, 0x48, 0x58));
887-
painter.drawEllipse(2, 1, 6, 6);
888-
painter.drawEllipse(2, 8, 6, 6);
889-
painter.drawEllipse(2, 15, 6, 6);
890-
return pixmap;
891-
}
892-
893664
static bool textInputCaptured()
894665
{
895666
auto* w = QApplication::focusWidget();
@@ -919,113 +690,6 @@ static bool leaseHolderBusy(QWidget* w) {
919690
return false;
920691
}
921692

922-
static QKeySequence shortcutSequenceFromKeyEvent(const QKeyEvent* ev)
923-
{
924-
if (!ev || ev->key() == Qt::Key_unknown)
925-
return {};
926-
927-
const Qt::KeyboardModifiers modifiers =
928-
ev->modifiers() & (Qt::ShiftModifier
929-
| Qt::ControlModifier
930-
| Qt::AltModifier
931-
| Qt::MetaModifier);
932-
return QKeySequence(static_cast<int>(modifiers) | ev->key());
933-
}
934-
935-
static QStringList splitClientField(const QString& raw)
936-
{
937-
QString cleaned = raw;
938-
cleaned.replace(QChar(0x7f), QLatin1Char(' '));
939-
940-
QStringList values;
941-
for (const QString& value : cleaned.split(',', Qt::SkipEmptyParts))
942-
values << value.trimmed();
943-
return values;
944-
}
945-
946-
static quint32 parseClientHandle(QString text)
947-
{
948-
text = text.trimmed();
949-
if (text.startsWith("0x", Qt::CaseInsensitive))
950-
text = text.mid(2);
951-
952-
bool ok = false;
953-
const quint32 handle = text.toUInt(&ok, 16);
954-
return ok ? handle : 0;
955-
}
956-
957-
static QList<ClientDisconnectDialog::Client> buildDisconnectClients(const QStringList& handles,
958-
const QStringList& programs,
959-
const QStringList& stations)
960-
{
961-
QList<ClientDisconnectDialog::Client> clients;
962-
for (int i = 0; i < handles.size(); ++i) {
963-
const quint32 handle = parseClientHandle(handles[i]);
964-
if (handle == 0)
965-
continue;
966-
967-
if (std::any_of(clients.cbegin(), clients.cend(), [handle](const auto& client) {
968-
return client.handle == handle;
969-
})) {
970-
continue;
971-
}
972-
973-
ClientDisconnectDialog::Client client;
974-
client.handle = handle;
975-
if (i < programs.size())
976-
client.program = programs[i];
977-
if (i < stations.size())
978-
client.station = stations[i];
979-
clients.append(client);
980-
}
981-
return clients;
982-
}
983-
984-
static QList<ClientDisconnectDialog::Client> buildDisconnectClients(const RadioInfo& info)
985-
{
986-
return buildDisconnectClients(info.guiClientHandles,
987-
info.guiClientPrograms,
988-
info.guiClientStations);
989-
}
990-
991-
static QList<ClientDisconnectDialog::Client> buildDisconnectClients(const WanRadioInfo& info)
992-
{
993-
return buildDisconnectClients(splitClientField(info.guiClientHandles),
994-
splitClientField(info.guiClientPrograms),
995-
splitClientField(info.guiClientStations));
996-
}
997-
998-
static QString cleanClientDisplayText(QString value)
999-
{
1000-
value.replace(QChar(0x7f), QLatin1Char(' '));
1001-
return value.trimmed();
1002-
}
1003-
1004-
static QString clientConnectionStatusMessage(quint32 handle,
1005-
const QString& source,
1006-
const QString& station,
1007-
const QString& program)
1008-
{
1009-
QString from = cleanClientDisplayText(source);
1010-
const QString stationText = cleanClientDisplayText(station);
1011-
const QString programText = cleanClientDisplayText(program);
1012-
QString detail = stationText;
1013-
1014-
if (detail.isEmpty() || detail.compare(QStringLiteral("Unknown"), Qt::CaseInsensitive) == 0)
1015-
detail = programText;
1016-
if (detail.compare(QStringLiteral("Unknown"), Qt::CaseInsensitive) == 0)
1017-
detail.clear();
1018-
1019-
if (from.isEmpty())
1020-
from = detail;
1021-
if (from.isEmpty())
1022-
from = QStringLiteral("client 0x%1").arg(handle, 8, 16, QChar('0')).toUpper();
1023-
1024-
if (!detail.isEmpty() && detail.compare(from, Qt::CaseInsensitive) != 0)
1025-
return QObject::tr("New client connection from %1 (%2)").arg(from, detail);
1026-
1027-
return QObject::tr("New client connection from %1").arg(from);
1028-
}
1029693

1030694
bool MainWindow::confirmClientSlotAvailability(const RadioInfo& info,
1031695
QList<quint32>* disconnectHandles)

0 commit comments

Comments
 (0)