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;'>● %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
815646static constexpr const char* kPaTempUnitSettingKey = "PaTempDisplayUnit";
816- static constexpr int kMemorySpotIdBase = 1000000;
817- static constexpr int kPassiveSpotIdBase = 2000000;
818647static constexpr const char* kCwStraightKeyActionId = "cwkey";
819648static constexpr const char* kCwLeftPaddleActionId = "cwdit";
820649static 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-
893664static 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
1030694bool MainWindow::confirmClientSlotAvailability(const RadioInfo& info,
1031695 QList<quint32>* disconnectHandles)
0 commit comments