js8call/JS8_Mainwindow/UI_Constructor.cpp
Chris-AC9KH 1ab08249b1 remove MainWindow class from mainwindow.cpp and replace it with UI_Constructor class
rename remaining files that didn't match their class definitions
relocate the explicit UI_Constructor member function to JS8_MainWindow
relocate the processBufferedActivity member function to JS8_MainWindow
format affected files with LLVM clang-format

Set default APRS inbound relay to off
Fix layout of General->Networking & Autoreply tab in Settings
2026-01-18 19:19:15 -06:00

1431 lines
59 KiB
C++

/**
* \file UI_Constructor.cpp
* @brief explicit member function of the UI_Constructor class
* constructs and connects UI elements to the JS8 "engine"
*/
#include "JS8_UI/mainwindow.h"
int ms_minute_error() {
auto const now = DriftingDateTime::currentDateTimeLocal();
auto const time = now.time();
auto const second = time.second();
return now.msecsTo(now.addSecs(second > 30 ? 60 - second : -second)) -
time.msec();
}
//--------------------------------------------------- UI_Constructor
UI_Constructor::UI_Constructor(QString const &program_info,
QDir const &temp_directory, bool const multiple,
MultiSettings *multi_settings, QWidget *parent)
: QMainWindow(parent), m_stopTxButtonIsLongterm{true},
m_hbButtonIsLongterm{true}, m_cqButtonIsLongterm{true},
m_network_manager{this}, m_valid{true}, m_multiple{multiple},
m_multi_settings{multi_settings}, m_configurations_button{0},
m_settings{multi_settings->settings()}, m_settings_read{false},
ui(new Ui::UI_Constructor), m_config{temp_directory, m_settings, this},
m_rigErrorMessageBox{JS8MessageBox::Critical, tr("Rig Control Error"),
JS8MessageBox::Cancel | JS8MessageBox::Ok |
JS8MessageBox::Retry},
m_wideGraph(new WideGraph(m_settings)),
// no parent so that it has a taskbar icon
m_logDlg(new LogQSO(program_title(), m_settings, &m_config, nullptr)),
m_lastDialFreq{0},
m_detector{new Detector{JS8_RX_SAMPLE_RATE, JS8_NTMAX}},
m_FFTSize{6912 / 2}, // conservative value to avoid buffer overruns
m_soundInput{new SoundInput}, m_modulator{new Modulator},
m_soundOutput{new SoundOutput}, m_notification{new NotificationAudio},
m_cq_loop{new TxLoop{"CQ calls"}}, m_hb_loop{new TxLoop{"HB calls"}},
m_decoder{this}, m_secBandChanged{0}, m_freqNominal{0},
m_freqTxNominal{0}, m_XIT{0}, m_sec0{-1},
m_RxLog{1}, // Write Date and Time to RxLog
m_nutc0{999999}, m_TRperiod{60}, m_inGain{0}, m_idleMinutes{0},
m_nSubMode{Default::SUBMODE},
m_frequency_list_fcal_iter{m_config.frequencies()->begin()}, m_i3bit{0},
m_btxok{false}, m_auto{false}, m_restart{false}, m_currentMessageType{-1},
m_lastMessageType{-1}, m_tuneup{false}, m_isTimeToSend{false}, m_ihsym{0},
m_px{0.0}, m_iptt0{0}, m_btxok0{false}, m_onAirFreq0{0.0},
m_first_error{true}, tx_status_label{"Receiving"},
m_appDir{QApplication::applicationDirPath()}, m_palette{"Linrad"},
m_txFrameCountEstimate{0}, m_txFrameCount{0}, m_txFrameCountSent{0},
m_txTextDirty{false}, m_driftMsMMA{0}, m_driftMsMMA_N{0},
m_previousFreq{0}, m_hbInterval{0}, m_cqInterval{0}, m_hbPaused{false},
m_msAudioOutputBuffered(0u),
m_framesAudioInputBuffered(JS8_RX_SAMPLE_RATE / 10),
m_audioThreadPriority(QThread::HighPriority),
m_notificationAudioThreadPriority(QThread::LowPriority),
m_decoderThreadPriority(QThread::HighPriority), m_splitMode{false},
m_monitoring{false}, m_generateAudioWhenPttConfirmedByTX{false},
m_transmitting{false}, m_tune{false}, m_tx_watchdog{false},
m_block_pwr_tooltip{false}, m_PwrBandSetOK{true},
m_lastMonitoredFrequency{Default::DIAL_FREQUENCY},
m_messageClient{new MessageClient{m_config.udp_server_name(),
m_config.udp_server_port(), this}},
m_messageServer{new MessageServer()}, m_wsjtxMessageClient{nullptr},
m_wsjtxMessageMapper{nullptr}, m_n3fjpClient{new TCPClient{this}},
m_pskReporter{new PSKReporter{&m_config, program_info}}, // UR
m_spotClient{new SpotClient{"spot.js8call.com", 50000, program_info}},
m_aprsClient{new APRSISClient{"rotate.aprs2.net", 14580}},
m_aprsInboundRelay{nullptr}, m_manual{&m_network_manager} {
ui->setupUi(this);
createStatusBar();
add_child_to_event_filter(this);
m_baseCall = Radio::base_callsign(m_config.my_callsign());
m_opCall = m_config.opCall();
// Closedown.
connect(ui->actionExit, &QAction::triggered, this, &QMainWindow::close);
// parts of the rig error message box that are fixed
m_rigErrorMessageBox.setInformativeText(
tr("Do you want to reconfigure the radio interface?"));
m_rigErrorMessageBox.setDefaultButton(JS8MessageBox::Ok);
// start audio thread and hook up slots & signals for shutdown management
// these objects need to be in the audio thread so that invoking
// their slots is done in a thread safe way
m_soundOutput->moveToThread(&m_audioThread);
m_modulator->moveToThread(&m_audioThread);
m_soundInput->moveToThread(&m_audioThread);
m_detector->moveToThread(&m_audioThread);
// notification audio operates in its own thread at a lower priority
m_notification->moveToThread(&m_notificationAudioThread);
// Move the aprs client message server, psk reporter, and spot client
// to the network thread at a lower priority.
m_aprsClient->moveToThread(&m_networkThread);
m_messageServer->moveToThread(&m_networkThread);
m_pskReporter->moveToThread(&m_networkThread);
m_spotClient->moveToThread(&m_networkThread);
// hook up the message server slots and signals and disposal
connect(m_messageServer, &MessageServer::message, this,
&UI_Constructor::tcpNetworkMessage);
connect(this, &UI_Constructor::apiSetMaxConnections, m_messageServer,
&MessageServer::setMaxConnections);
connect(this, &UI_Constructor::apiSetServer, m_messageServer,
&MessageServer::setServer);
connect(this, &UI_Constructor::apiStartServer, m_messageServer,
&MessageServer::start);
connect(this, &UI_Constructor::apiStopServer, m_messageServer,
&MessageServer::stop);
connect(&m_config, &Configuration::tcp_server_changed, m_messageServer,
&MessageServer::setServerHost);
connect(&m_config, &Configuration::tcp_server_port_changed, m_messageServer,
&MessageServer::setServerPort);
connect(&m_config, &Configuration::tcp_max_connections_changed,
m_messageServer, &MessageServer::setMaxConnections);
connect(&m_networkThread, &QThread::finished, m_messageServer,
&QObject::deleteLater);
m_aprsInboundRelay = new AprsInboundRelay(
&m_config,
[this](QString const &call) {
AprsInboundRelay::CallActivityInfo info;
auto const it = m_callActivity.constFind(call);
if (it == m_callActivity.constEnd()) {
return info;
}
info.heard = true;
info.lastHeardUtc = it->utcTimestamp;
return info;
},
[this](QDateTime const &utc, QString const &text) {
writeNoticeTextToUI(utc, text);
},
[this](QString const &relayMsg) {
enqueueMessage(PriorityHigh, relayMsg, -1, nullptr);
},
this);
// hook up the aprs client slots and signals and disposal
connect(this, &UI_Constructor::aprsClientEnqueueSpot, m_aprsClient,
&APRSISClient::enqueueSpot);
connect(this, &UI_Constructor::aprsClientEnqueueThirdParty, m_aprsClient,
&APRSISClient::enqueueThirdParty);
connect(this, &UI_Constructor::aprsClientSendReports, m_aprsClient,
&APRSISClient::sendReports);
connect(this, &UI_Constructor::aprsClientSetLocalStation, m_aprsClient,
&APRSISClient::setLocalStation);
connect(this, &UI_Constructor::aprsClientSetPaused, m_aprsClient,
&APRSISClient::setPaused);
connect(this, &UI_Constructor::aprsClientSetServer, m_aprsClient,
&APRSISClient::setServer);
connect(this, &UI_Constructor::aprsClientSetSkipPercent, m_aprsClient,
&APRSISClient::setSkipPercent);
connect(this, &UI_Constructor::aprsClientSetIncomingRelayEnabled,
m_aprsClient, &APRSISClient::setIncomingRelayEnabled);
connect(&m_config, &Configuration::spot_to_aprs_relay_changed, m_aprsClient,
&APRSISClient::setIncomingRelayEnabled);
connect(m_aprsClient, &APRSISClient::messageReceived, m_aprsInboundRelay,
&AprsInboundRelay::onMessageReceived);
connect(&m_networkThread, &QThread::finished, m_aprsClient,
&QObject::deleteLater);
// hook up the psk reporter slots and signals and disposal
connect(m_pskReporter, &PSKReporter::errorOccurred, this,
&UI_Constructor::pskReporterError);
connect(this, &UI_Constructor::pskReporterSendReport, m_pskReporter,
&PSKReporter::sendReport);
connect(this, &UI_Constructor::pskReporterAddRemoteStation, m_pskReporter,
&PSKReporter::addRemoteStation);
connect(this, &UI_Constructor::pskReporterSetLocalStation, m_pskReporter,
&PSKReporter::setLocalStation);
connect(&m_networkThread, &QThread::started, m_pskReporter,
&PSKReporter::start);
connect(&m_networkThread, &QThread::finished, m_pskReporter,
&QObject::deleteLater);
// hook up the spot client signals and disposal
connect(this, &UI_Constructor::spotClientEnqueueCmd, m_spotClient,
&SpotClient::enqueueCmd);
connect(this, &UI_Constructor::spotClientEnqueueSpot, m_spotClient,
&SpotClient::enqueueSpot);
connect(this, &UI_Constructor::spotClientSetLocalStation, m_spotClient,
&SpotClient::setLocalStation);
connect(&m_networkThread, &QThread::started, m_spotClient,
&SpotClient::start);
connect(&m_networkThread, &QThread::finished, m_spotClient,
&QObject::deleteLater);
// hook up sound output stream slots & signals and disposal
connect(this, &UI_Constructor::initializeAudioOutputStream, m_soundOutput,
&SoundOutput::setFormat);
connect(m_soundOutput, &SoundOutput::error, this,
&UI_Constructor::showSoundOutError);
connect(m_soundOutput, &SoundOutput::error, &m_config,
&Configuration::invalidate_audio_output_device);
connect(this, &UI_Constructor::outAttenuationChanged, m_soundOutput,
&SoundOutput::setAttenuation);
connect(&m_audioThread, &QThread::finished, m_soundOutput,
&QObject::deleteLater);
connect(this, &UI_Constructor::initializeNotificationAudioOutputStream,
m_notification, &NotificationAudio::setDevice);
connect(&m_config, &Configuration::test_notify, this,
&UI_Constructor::tryNotify);
connect(this, &UI_Constructor::playNotification, m_notification,
&NotificationAudio::play);
connect(&m_notificationAudioThread, &QThread::finished, m_notification,
&QObject::deleteLater);
// hook up Modulator slots and disposal
connect(this, &UI_Constructor::transmitFrequency, m_modulator,
&Modulator::setAudioFrequency);
connect(this, &UI_Constructor::endTransmitMessage, m_modulator,
&Modulator::stop);
connect(this, &UI_Constructor::tune, m_modulator, &Modulator::tune);
connect(this, &UI_Constructor::sendMessage, m_modulator, &Modulator::start);
connect(&m_audioThread, &QThread::finished, m_modulator,
&QObject::deleteLater);
// hook up the audio input stream signals, slots and disposal
connect(this, &UI_Constructor::startAudioInputStream, m_soundInput,
&SoundInput::start);
connect(this, &UI_Constructor::suspendAudioInputStream, m_soundInput,
&SoundInput::suspend);
connect(this, &UI_Constructor::resumeAudioInputStream, m_soundInput,
&SoundInput::resume);
connect(this, &UI_Constructor::finished, m_soundInput, &SoundInput::stop);
connect(m_soundInput, &SoundInput::error, this,
&UI_Constructor::showSoundInError);
connect(m_soundInput, &SoundInput::error, &m_config,
&Configuration::invalidate_audio_input_device);
// connect(m_soundInput, &SoundInput::status, this,
// &UI_Constructor::showStatusMessage);
connect(&m_audioThread, &QThread::finished, m_soundInput,
&QObject::deleteLater);
connect(this, &UI_Constructor::finished, this, &UI_Constructor::close);
// hook up the detector signals, slots and disposal
connect(this, &UI_Constructor::FFTSize, m_detector,
&Detector::setBlockSize);
connect(m_detector, &Detector::framesWritten, this,
&UI_Constructor::dataSink);
connect(&m_audioThread, &QThread::finished, m_detector,
&QObject::deleteLater);
// setup the waterfall
connect(m_wideGraph.data(), &WideGraph::f11f12, this,
&UI_Constructor::f11f12);
connect(m_wideGraph.data(), &WideGraph::setXIT, this,
&UI_Constructor::setXIT);
connect(this, &UI_Constructor::finished, m_wideGraph.data(),
&WideGraph::close);
// setup the log QSO dialog
connect(m_logDlg.data(), &LogQSO::acceptQSO, this,
&UI_Constructor::acceptQSO);
connect(this, &UI_Constructor::finished, m_logDlg.data(), &LogQSO::close);
// Network message handling
connect(m_messageClient, &MessageClient::message, this,
&UI_Constructor::udpNetworkMessage);
/**
* @brief Initialize WSJT-X protocol if enabled
*
* Creates and configures the WSJT-X message client and mapper when the
* WSJT-X protocol is enabled. Sets up signal connections for configuration
* changes and disables the native JSON client if it conflicts with WSJT-X
* on the same port/address.
*/
if (m_config.wsjtx_protocol_enabled()) {
QString id = QApplication::applicationName();
QString version = QApplication::applicationVersion();
QString revision = ""; // Get from your version system if available
m_wsjtxMessageClient = new WSJTXMessageClient{
id,
version,
revision,
m_config.wsjtx_server_name(),
m_config.wsjtx_server_port(),
m_config.wsjtx_interface_names(), // Use selected interfaces
m_config.wsjtx_TTL(),
this};
m_wsjtxMessageClient->enable(m_config.wsjtx_accept_requests());
m_wsjtxMessageMapper =
new WSJTXMessageMapper(m_wsjtxMessageClient, this, this);
// Disable native JSON client if it's using the same port/address as
// WSJT-X
if (m_config.wsjtx_server_port() == m_config.udp_server_port() &&
m_config.wsjtx_server_name() == m_config.udp_server_name()) {
m_messageClient->set_server_port(0); // Disable native JSON client
}
// Connect configuration changes
connect(&m_config, &Configuration::wsjtx_server_changed,
[this](QString const &server_name) {
m_wsjtxMessageClient->set_server(
server_name, m_config.wsjtx_interface_names());
// Check if we need to disable native JSON client
if (m_config.wsjtx_protocol_enabled() &&
m_config.wsjtx_server_port() ==
m_config.udp_server_port() &&
server_name == m_config.udp_server_name()) {
m_messageClient->set_server_port(0);
} else if (m_config.wsjtx_protocol_enabled() &&
m_config.wsjtx_server_port() !=
m_config.udp_server_port()) {
m_messageClient->set_server_port(
m_config.udp_server_port());
}
});
connect(&m_config, &Configuration::wsjtx_server_port_changed,
[this](quint16 port) {
m_wsjtxMessageClient->set_server_port(port);
// Check if we need to disable native JSON client
if (m_config.wsjtx_protocol_enabled() &&
port == m_config.udp_server_port() &&
m_config.wsjtx_server_name() ==
m_config.udp_server_name()) {
m_messageClient->set_server_port(0);
} else if (m_config.wsjtx_protocol_enabled() &&
port != m_config.udp_server_port()) {
m_messageClient->set_server_port(
m_config.udp_server_port());
}
});
connect(&m_config, &Configuration::wsjtx_TTL_changed, this,
[this](int ttl) {
if (m_wsjtxMessageClient) {
m_wsjtxMessageClient->set_TTL(ttl);
}
});
connect(&m_config, &Configuration::wsjtx_interfaces_changed,
[this](QStringList const &interfaces) {
if (m_wsjtxMessageClient) {
m_wsjtxMessageClient->set_server(
m_config.wsjtx_server_name(), interfaces);
}
});
}
// decoder queue handler
// connect (&m_decodeThread, &QThread::finished, m_notification,
// &QObject::deleteLater); connect(this, &UI_Constructor::decodedLineReady,
// this, &UI_Constructor::processDecodedLine);
connect(&m_decoder, &JS8::Decoder::decodeEvent, this,
&UI_Constructor::processDecodeEvent);
m_dateTimeQSOOn = QDateTime{};
// initialize decoded text font and hook up font change signals
// defer initialization until after construction otherwise menu
// fonts do not get set
QTimer::singleShot(0, this, &UI_Constructor::initialize_fonts);
connect(&m_config, &Configuration::gui_text_font_changed,
[this](QFont const &font) { set_application_font(font); });
connect(&m_config, &Configuration::table_font_changed,
[this](QFont const &) {
ui->tableWidgetRXAll->setFont(m_config.table_font());
ui->tableWidgetCalls->setFont(m_config.table_font());
});
connect(&m_config, &Configuration::rx_text_font_changed,
[this](QFont const &) {
setTextEditFont(ui->textEditRX, m_config.rx_text_font());
});
connect(&m_config, &Configuration::compose_text_font_changed,
[this](QFont const &) {
setTextEditFont(ui->extFreeTextMsgEdit,
m_config.compose_text_font());
});
connect(&m_config, &Configuration::colors_changed, [this]() {
setTextEditStyle(ui->textEditRX, m_config.color_rx_foreground(),
m_config.color_rx_background(),
m_config.rx_text_font());
setTextEditStyle(
ui->extFreeTextMsgEdit, m_config.color_compose_foreground(),
m_config.color_compose_background(), m_config.compose_text_font());
ui->extFreeTextMsgEdit->setFont(m_config.compose_text_font(),
m_config.color_compose_foreground(),
m_config.color_compose_background());
// rehighlight
auto d = ui->textEditRX->document();
if (d) {
for (int i = 0; i < d->lineCount(); i++) {
auto b = d->findBlockByLineNumber(i);
switch (b.userState()) {
case State::RX:
highlightBlock(b, m_config.rx_text_font(),
m_config.color_rx_foreground(),
QColor(Qt::transparent));
break;
case State::TX:
highlightBlock(b, m_config.tx_text_font(),
m_config.color_tx_foreground(),
QColor(Qt::transparent));
break;
}
}
}
});
setWindowTitle(program_title());
buildColumnLabelMap();
// Hook up working frequencies.
ui->currentFreq->setCursor(QCursor(Qt::PointingHandCursor));
ui->currentFreq->display("14.078 000");
ui->currentFreq->installEventFilter(new EventFilter::MouseButtonPress(
[this](QMouseEvent *event) {
QMenu *menu = new QMenu(ui->currentFreq);
buildFrequencyMenu(menu);
menu->popup(event->globalPosition().toPoint());
return true;
},
this));
ui->labDialFreqOffset->setCursor(QCursor(Qt::PointingHandCursor));
ui->labDialFreqOffset->installEventFilter(new EventFilter::MouseButtonPress(
[this](QMouseEvent *) {
on_actionSetOffset_triggered();
return true;
},
this));
// Hook up callsign label click to open preferences
ui->labCallsign->setCursor(QCursor(Qt::PointingHandCursor));
ui->labCallsign->installEventFilter(new EventFilter::MouseButtonPress(
[this](QMouseEvent *) {
openSettings(0);
return true;
},
this));
// hook up configuration signals
connect(&m_config, &Configuration::transceiver_update, this,
&UI_Constructor::handle_transceiver_update);
connect(&m_config, &Configuration::transceiver_failure, this,
&UI_Constructor::handle_transceiver_failure);
connect(&m_config, &Configuration::udp_server_name_changed, m_messageClient,
&MessageClient::set_server_name);
connect(&m_config, &Configuration::udp_server_port_changed, m_messageClient,
&MessageClient::set_server_port);
// Disable native JSON client if WSJT-X protocol is enabled on the same
// port/address This prevents JSON PING messages from interfering with
// WSJT-X binary protocol
connect(
&m_config, &Configuration::wsjtx_protocol_enabled_changed, this,
[this](bool enabled) {
if (enabled &&
m_config.wsjtx_server_port() == m_config.udp_server_port() &&
m_config.wsjtx_server_name() == m_config.udp_server_name()) {
// Disable native JSON client to avoid conflicts with WSJT-X
// protocol
m_messageClient->set_server_port(0);
} else if (!enabled) {
// Re-enable native JSON client if WSJT-X is disabled
m_messageClient->set_server_port(m_config.udp_server_port());
}
});
connect(&m_config, &Configuration::band_schedule_changed, this,
[this]() { this->m_bandHopped = true; });
connect(&m_config, &Configuration::auto_switch_bands_changed, this,
[this](bool auto_switch_bands) {
this->m_bandHopped = this->m_bandHopped || auto_switch_bands;
});
connect(&m_config, &Configuration::manual_band_hop_requested, this,
&UI_Constructor::manualBandHop);
connect(&m_config, &Configuration::enumerating_audio_devices,
[this]() { showStatusMessage(tr("Enumerating audio devices")); });
// set up configurations menu
connect(m_multi_settings, &MultiSettings::configurationNameChanged,
[this](QString const &name) {
if ("Default" != name) {
config_label.setText(name);
config_label.show();
} else {
config_label.hide();
}
});
m_multi_settings->create_menu_actions(this, ui->menuConfig);
m_configurations_button = m_rigErrorMessageBox.addButton(
tr("Configurations..."), QMessageBox::ActionRole);
connect(ui->extFreeTextMsgEdit, &QTextEdit::textChanged,
[this]() { currentTextChanged(); });
m_guiTimer.setTimerType(Qt::PreciseTimer);
m_guiTimer.setSingleShot(true);
connect(&m_guiTimer, &QTimer::timeout, this, &UI_Constructor::guiUpdate);
m_guiTimer.start(UI_POLL_INTERVAL_MS);
pttReleaseTimer.setTimerType(Qt::PreciseTimer);
pttReleaseTimer.setSingleShot(true);
connect(&pttReleaseTimer, &QTimer::timeout, this, &UI_Constructor::stopTx2);
logQSOTimer.setSingleShot(true);
connect(&logQSOTimer, &QTimer::timeout, this,
&UI_Constructor::on_logQSOButton_clicked);
tuneButtonTimer.setSingleShot(true);
connect(&tuneButtonTimer, &QTimer::timeout, this,
&UI_Constructor::end_tuning);
tuneATU_Timer.setSingleShot(true);
connect(&tuneATU_Timer, &QTimer::timeout, this,
&UI_Constructor::stopTuneATU);
TxAgainTimer.setSingleShot(true);
connect(&TxAgainTimer, &QTimer::timeout, this, &UI_Constructor::TxAgain);
connect(m_wideGraph.data(), &WideGraph::changeFreq, this,
&UI_Constructor::changeFreq);
connect(m_wideGraph.data(), &WideGraph::qsy, this, &UI_Constructor::qsy);
// DriftingDateTime management:
connect(m_wideGraph.data(), &WideGraph::want_new_drift,
&DriftingDateTimeSingleton::getSingleton(),
&DriftingDateTimeSingleton::setDrift);
// Distribute Drift change:
connect(&DriftingDateTimeSingleton::getSingleton(),
&DriftingDateTimeSingleton::driftChanged, this,
&UI_Constructor::onDriftChanged);
connect(&DriftingDateTimeSingleton::getSingleton(),
&DriftingDateTimeSingleton::driftChanged, m_wideGraph.data(),
&WideGraph::onDriftChanged);
connect(&DriftingDateTimeSingleton::getSingleton(),
&DriftingDateTimeSingleton::driftChanged, m_cq_loop,
&TxLoop::onDriftChange);
connect(&DriftingDateTimeSingleton::getSingleton(),
&DriftingDateTimeSingleton::driftChanged, m_hb_loop,
&TxLoop::onDriftChange);
// HB and CQ loop:
// For now, disable HB loop while CQ loop runs and vice versa:
connect(m_cq_loop, &TxLoop::nextActivityChanged, this,
[this](const QDateTime &) { this->m_hb_loop->onLoopCancel(); });
connect(m_hb_loop, &TxLoop::nextActivityChanged, this,
[this](const QDateTime &) { this->m_cq_loop->onLoopCancel(); });
// It is not advisable to send a HB in one period and a CQ in the very next,
// or a CQ first and then a HB too soon, so that transmissions of people
// interested in the QSO might be drowned.
//
// We could conceivably devise a clean conflict resolution mechanism
// disallowing automatic transmissions of one type if an automatic
// transmission of the other type has happened recently. In the absence of
// such a mechanism, just do either one or the other.
//
// People who call CQ are expected to monitor somewhat closely, so they will
// have no problems triggering HBs at will.
// Propagate tx submode changes to the CQ and HB loop:
connect(this, &UI_Constructor::submodeChanged, this->m_hb_loop,
&TxLoop::onModeChange);
connect(this, &UI_Constructor::submodeChanged, this->m_cq_loop,
&TxLoop::onModeChange);
// When the loops are switched off, tell the UI:
connect(m_hb_loop, &TxLoop::canceled, ui->hbMacroButton,
[this]() { this->ui->hbMacroButton->setChecked(false); });
connect(m_cq_loop, &TxLoop::canceled, ui->cqMacroButton,
[this]() { this->ui->cqMacroButton->setChecked(false); });
// The loops can trigger transmissions. That is what they are for.
connect(m_hb_loop, &TxLoop::triggerTxNow, this,
[this]() { this->sendHB(); });
connect(m_cq_loop, &TxLoop::triggerTxNow, this,
[this]() { this->sendCQ(true); });
// Something like this would be nice to have:
// connect(m_config, &Configuration::txDelayChanged, m_cq_loop,
// &TxLoop::onTxDelayChange); connect(m_config,
// &Configuration::txDelayChanged, m_hb_loop, &TxLoop::onTxDelayChange); But
// the pertaining signals are not offered by Configuration, and the code was
// somewhat of a beast to get into to change that, so some equivalent is
// done in a pedestrian way via our polling routine. That pedestrian code
// also handles conversion of incoming tx delay in (double) seconds to
// outgoing (qint64) milliseconds.
decodeBusy(false);
m_msg[0][0] = 0;
displayDialFrequency();
readSettings(); // Restore user's setup params
{
std::lock_guard<std::mutex> lock(fftw_mutex);
fftwf_import_wisdom_from_filename(wisdomFileName());
}
m_networkThread.start(m_networkThreadPriority);
m_audioThread.start(m_audioThreadPriority);
m_notificationAudioThread.start(m_notificationAudioThreadPriority);
m_decoder.start(m_decoderThreadPriority);
Q_EMIT startAudioInputStream(m_config.audio_input_device(),
m_framesAudioInputBuffered, m_detector,
m_config.audio_input_channel());
Q_EMIT initializeAudioOutputStream(
m_config.audio_output_device(),
AudioDevice::Mono == m_config.audio_output_channel() ? 1 : 2,
m_msAudioOutputBuffered);
Q_EMIT initializeNotificationAudioOutputStream(
m_config.notification_audio_output_device(), m_msAudioOutputBuffered);
Q_EMIT transmitFrequency(freq() + m_XIT);
enable_DXCC_entity(
m_config
.DXCC()); // sets text window proportions and (re)inits the logbook
// this must be done before initializing the mode as some modes need
// to turn off split on the rig e.g. WSPR
m_config.transceiver_online();
setupJS8();
Q_EMIT transmitFrequency(freq() + m_XIT);
statusChanged();
connect(&minuteTimer, &QTimer::timeout, this,
&UI_Constructor::on_the_minute);
minuteTimer.setSingleShot(true);
minuteTimer.start(ms_minute_error() + 60 * 1000);
QTimer::singleShot(0, this, &UI_Constructor::checkStartupWarnings);
// UI Customizations & Tweaks
ui->horizontalLayoutBand->insertSpacing(1, 6);
ui->horizontalLayoutBand->insertWidget(2, m_wideGraph.data(), 1);
ui->horizontalLayoutBand->insertSpacing(3, 8);
// remove disabled menus from the menu bar
foreach (auto action, ui->menuBar->actions()) {
if (action->isEnabled()) {
continue;
}
ui->menuBar->removeAction(action);
}
// auto f = findFreeFreqOffset(1000, 2000, 50);
// setFreqOffsetForRestore(f, false);
ui->actionModeAutoreply->setChecked(m_config.autoreply_on_at_startup());
ui->spotButton->setChecked(m_config.spot_to_reporting_networks());
QActionGroup *modeActionGroup = new QActionGroup(this);
ui->actionModeJS8Normal->setActionGroup(modeActionGroup);
ui->actionModeJS8Fast->setActionGroup(modeActionGroup);
ui->actionModeJS8Turbo->setActionGroup(modeActionGroup);
ui->actionModeJS8Slow->setActionGroup(modeActionGroup);
ui->actionModeJS8Ultra->setActionGroup(modeActionGroup);
ui->modeButton->installEventFilter(new EventFilter::MouseButtonPress(
[this](QMouseEvent *event) {
ui->menuModeJS8->popup(event->globalPosition().toPoint());
return true;
},
this));
if (!JS8_ENABLE_JS8A)
ui->actionModeJS8Normal->setVisible(false);
if (!JS8_ENABLE_JS8B)
ui->actionModeJS8Fast->setVisible(false);
if (!JS8_ENABLE_JS8C)
ui->actionModeJS8Turbo->setVisible(false);
if (!JS8_ENABLE_JS8E)
ui->actionModeJS8Slow->setVisible(false);
if (!JS8_ENABLE_JS8I)
ui->actionModeJS8Ultra->setVisible(false);
// prep
prepareMonitorControls();
prepareHeartbeatMode(canCurrentModeSendHeartbeat() &&
ui->actionModeJS8HB->isChecked());
ui->extFreeTextMsgEdit->installEventFilter(new EventFilter::EnterKeyPress(
[this](QKeyEvent *const event) {
if (event->modifiers() & Qt::ShiftModifier)
return false;
if (ui->extFreeTextMsgEdit->isReadOnly())
return false;
if (ui->extFreeTextMsgEdit->toPlainText().trimmed().isEmpty())
return true;
if (!ensureCanTransmit())
return true;
if (!ensureCallsignSet(true))
return true;
toggleTx(true);
return true;
},
this));
ui->textEditRX->viewport()->installEventFilter(
new EventFilter::MouseButtonDblClick(
[this](QMouseEvent *) {
QTimer::singleShot(150, this, [this]() {
// When we double click the rx window, we send the selected
// text to the log dialog when the text could be an snr
// value prefixed with a - or +, we extend the selection to
// include it.
auto textCursor = ui->textEditRX->textCursor();
auto text = textCursor.selectedText();
if (text.isEmpty())
return;
auto const start = textCursor.selectionStart();
auto const end = textCursor.selectionEnd();
textCursor.clearSelection();
textCursor.setPosition(start);
textCursor.movePosition(QTextCursor::PreviousCharacter,
QTextCursor::MoveAnchor);
textCursor.movePosition(QTextCursor::NextCharacter,
QTextCursor::KeepAnchor,
1 + end - start);
if (auto const prev = textCursor.selectedText();
prev.startsWith("-") || prev.startsWith("+")) {
ui->textEditRX->setTextCursor(textCursor);
text = prev;
}
m_logDlg->acceptText(text);
});
return false;
},
this));
auto clearActionSep = new QAction(nullptr);
clearActionSep->setSeparator(true);
auto clearActionAll = new QAction(QString("Clear All Lists"), nullptr);
connect(clearActionAll, &QAction::triggered, this, [this]() {
if (QMessageBox::Yes !=
QMessageBox::question(
this, "Clear All Activity",
"Are you sure you would like to clear all activity?",
QMessageBox::Yes | QMessageBox::No)) {
return;
}
clearActivity();
});
// setup tablewidget context menus
auto clearAction1 = new QAction(QString("Clear"), ui->textEditRX);
connect(clearAction1, &QAction::triggered, this,
[this]() { clearRXActivity(); });
auto saveAction = new QAction(QString("Save As..."), ui->textEditRX);
connect(saveAction, &QAction::triggered, this, [this]() {
auto writePath =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
auto writeDir = QDir(writePath);
auto defaultFilename = writeDir.absoluteFilePath(
QString("js8call-%1.txt")
.arg(DriftingDateTime::currentDateTimeUtc().toString(
"yyyyMMdd")));
QString selectedFilter = "*.txt";
auto filename = QFileDialog::getSaveFileName(
this, "Save As...", defaultFilename,
"Text files (*.txt);; All files (*)", &selectedFilter);
if (filename.isEmpty()) {
return;
}
auto text = ui->textEditRX->toPlainText();
QFile f(filename);
if (f.open(QIODevice::Truncate | QIODevice::WriteOnly |
QIODevice::Text)) {
QTextStream stream(&f);
stream << text;
}
});
ui->textEditRX->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
ui->textEditRX, &QTableWidget::customContextMenuRequested, this,
[this, clearAction1, clearActionAll, saveAction](QPoint const &point) {
QMenu *menu = new QMenu(ui->textEditRX);
buildEditMenu(menu, ui->textEditRX);
menu->addSeparator();
menu->addAction(clearAction1);
menu->addAction(clearActionAll);
menu->addSeparator();
menu->addAction(saveAction);
menu->popup(ui->textEditRX->mapToGlobal(point));
});
auto clearAction2 = new QAction(QString("Clear"), ui->extFreeTextMsgEdit);
connect(clearAction2, &QAction::triggered, this, [this]() {
resetMessage();
m_lastTxMessage.clear();
});
auto restoreAction = new QAction(QString("Restore Previous Message"),
ui->extFreeTextMsgEdit);
connect(restoreAction, &QAction::triggered, this,
[this]() { this->restoreMessage(); });
ui->extFreeTextMsgEdit->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
ui->extFreeTextMsgEdit, &QTableWidget::customContextMenuRequested, this,
[this, clearAction2, clearActionAll,
restoreAction](QPoint const &point) {
QMenu *menu = new QMenu(ui->extFreeTextMsgEdit);
auto selectedCall = callsignSelected();
bool missingCallsign = selectedCall.isEmpty();
buildSuggestionsMenu(menu, ui->extFreeTextMsgEdit, point);
restoreAction->setDisabled(m_lastTxMessage.isEmpty());
menu->addAction(restoreAction);
auto savedMenu = menu->addMenu("Saved Messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu =
menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayMenu = menu->addMenu("Relay via...");
relayMenu->setDisabled(
ui->extFreeTextMsgEdit->toPlainText().isEmpty() ||
m_callActivity.isEmpty());
buildRelayMenu(relayMenu);
menu->addSeparator();
buildEditMenu(menu, ui->extFreeTextMsgEdit);
menu->addSeparator();
menu->addAction(clearAction2);
menu->addAction(clearActionAll);
menu->popup(ui->extFreeTextMsgEdit->mapToGlobal(point));
displayActivity(true);
});
auto clearAction3 = new QAction(QString("Clear"), ui->tableWidgetRXAll);
connect(clearAction3, &QAction::triggered, this,
[this]() { clearBandActivity(); });
auto removeActivity =
new QAction(QString("Remove Activity"), ui->tableWidgetRXAll);
connect(removeActivity, &QAction::triggered, this, [this]() {
if (ui->tableWidgetRXAll->selectedItems().isEmpty()) {
return;
}
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
int selectedOffset = selectedItems.first()->data(Qt::UserRole).toInt();
m_bandActivity.remove(selectedOffset);
displayActivity(true);
});
auto logAction = new QAction(QString("Log..."), ui->tableWidgetCalls);
connect(logAction, &QAction::triggered, this,
&UI_Constructor::on_logQSOButton_clicked);
// Disable default header mouseover and click behaviors, they are confusing
// to users because they give the appearance of allowing sorting by header
// clicks, which is not actually implemented
ui->tableWidgetRXAll->horizontalHeader()->setHighlightSections(false);
ui->tableWidgetRXAll->horizontalHeader()->setSectionsClickable(false);
ui->tableWidgetRXAll->horizontalHeader()->setContextMenuPolicy(
Qt::CustomContextMenu);
connect(
ui->tableWidgetRXAll->horizontalHeader(),
&QHeaderView::customContextMenuRequested, this,
[this](QPoint const &point) {
QMenu *menu = new QMenu(ui->tableWidgetRXAll);
QMenu *sortByMenu = menu->addMenu("Sort By...");
buildBandActivitySortByMenu(sortByMenu);
QMenu *showColumnsMenu = menu->addMenu("Show Columns...");
buildShowColumnsMenu(showColumnsMenu, "band");
menu->popup(
ui->tableWidgetRXAll->horizontalHeader()->mapToGlobal(point));
});
ui->tableWidgetRXAll->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
ui->tableWidgetRXAll, &QTableWidget::customContextMenuRequested, this,
[this, clearAction3, clearActionAll, removeActivity,
logAction](QPoint const &point) {
QMenu *menu = new QMenu(ui->tableWidgetRXAll);
// clear the selection of the call widget on right click
// but only if the table has rows.
if (ui->tableWidgetRXAll->rowAt(point.y()) != -1) {
ui->tableWidgetCalls->selectionModel()->clearSelection();
}
QString selectedCall = callsignSelected();
bool missingCallsign = selectedCall.isEmpty();
bool isAllCall = isAllCallIncluded(selectedCall);
int selectedOffset = -1;
if (!ui->tableWidgetRXAll->selectedItems().isEmpty()) {
auto selectedItems = ui->tableWidgetRXAll->selectedItems();
selectedOffset =
selectedItems.first()->data(Qt::UserRole).toInt();
}
if (selectedOffset != -1) {
auto qsyAction = menu->addAction(
QString("Jump to %1Hz").arg(selectedOffset));
connect(qsyAction, &QAction::triggered, this,
[this, selectedOffset]() {
setFreqOffsetForRestore(selectedOffset, false);
});
if (m_wideGraph->filterEnabled()) {
auto filterQsyAction = menu->addAction(
QString("Center filter at %1Hz").arg(selectedOffset));
connect(filterQsyAction, &QAction::triggered, this,
[this, selectedOffset]() {
m_wideGraph->setFilterCenter(selectedOffset);
});
}
auto items = m_bandActivity.value(selectedOffset);
if (!items.isEmpty()) {
int submode = items.last().submode;
auto speed = JS8::Submode::name(submode);
if (submode != m_nSubMode) {
auto qrqAction =
menu->addAction(QString("Jump to %1%2 speed")
.arg(speed.left(1))
.arg(speed.mid(1).toLower()));
connect(qrqAction, &QAction::triggered, this,
[this, submode]() { setSubmode(submode); });
}
int tdrift = -int(items.last().tdrift * 1000);
auto qtrAction = menu->addAction(
QString("Jump to %1 ms time drift").arg(tdrift));
connect(qtrAction, &QAction::triggered, this,
[this, tdrift]() { setDrift(tdrift); });
}
menu->addSeparator();
}
menu->addAction(logAction);
logAction->setDisabled(missingCallsign || isAllCall);
menu->addSeparator();
auto savedMenu = menu->addMenu("Saved Messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu =
menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayAction = buildRelayAction(selectedCall);
relayAction->setText(QString("Relay via %1...").arg(selectedCall));
relayAction->setDisabled(missingCallsign);
menu->addActions({relayAction});
auto deselectAction =
menu->addAction(QString("Deselect %1").arg(selectedCall));
deselectAction->setDisabled(missingCallsign);
connect(deselectAction, &QAction::triggered, this, [this]() {
ui->tableWidgetRXAll->clearSelection();
ui->tableWidgetCalls->clearSelection();
});
menu->addSeparator();
removeActivity->setDisabled(selectedOffset == -1);
menu->addAction(removeActivity);
menu->addSeparator();
menu->addAction(clearAction3);
menu->addAction(clearActionAll);
menu->popup(ui->tableWidgetRXAll->mapToGlobal(point));
displayActivity(true);
});
auto clearAction4 =
new QAction(QString("Clear Entire List"), ui->tableWidgetCalls);
connect(clearAction4, &QAction::triggered, this,
[this]() { clearCallActivity(); });
auto addStation = new QAction(QString("Add New Station or Group..."),
ui->tableWidgetCalls);
connect(addStation, &QAction::triggered, this, [this]() {
bool ok = false;
QString callsign =
QInputDialog::getText(this, tr("Add New Station or Group"),
tr("Station or Group Callsign:"),
QLineEdit::Normal, "", &ok)
.toUpper()
.trimmed();
if (!ok || callsign.trimmed().isEmpty()) {
return;
}
// if we're adding allcall, turn off allcall avoidance
if (callsign == "@ALLCALL") {
m_config.set_avoid_allcall(false);
} else if (callsign.startsWith("@")) {
if (Varicode::isCompoundCallsign(callsign)) {
m_config.addGroup(callsign);
} else {
JS8MessageBox::critical_message(
this, QString("%1 is not a valid group").arg(callsign));
}
} else {
if (Varicode::isValidCallsign(callsign, nullptr)) {
CallDetail cd = {};
cd.call = callsign;
m_callActivity[callsign] = cd;
} else {
JS8MessageBox::critical_message(
this, QString("%1 is not a valid callsign or group")
.arg(callsign));
}
}
displayActivity(true);
});
auto removeStation =
new QAction(QString("Remove Station"), ui->tableWidgetCalls);
connect(removeStation, &QAction::triggered, this, [this]() {
QString selectedCall = callsignSelected();
if (selectedCall.isEmpty()) {
return;
}
if (selectedCall == "@ALLCALL") {
m_config.set_avoid_allcall(true);
} else if (selectedCall.startsWith("@")) {
m_config.removeGroup(selectedCall);
} else if (m_callActivity.contains(selectedCall)) {
m_callActivity.remove(selectedCall);
}
displayActivity(true);
});
connect(ui->actionShow_Message_Inbox, &QAction::triggered, this, [this]() {
QString selectedCall = callsignSelected();
if (selectedCall.isEmpty()) {
selectedCall = "%";
}
Inbox inbox(inboxPath());
if (!inbox.open()) {
return;
}
QList<QPair<int, Message>> msgs;
msgs.append(
inbox.values("STORE", "$.params.TO", selectedCall, 0, 1000));
msgs.append(
inbox.values("READ", "$.params.FROM", selectedCall, 0, 1000));
foreach (auto pair, inbox.values("UNREAD", "$.params.FROM",
selectedCall, 0, 1000)) {
msgs.append(pair);
// mark as read
auto msg = pair.second;
msg.setType("READ");
inbox.set(pair.first, msg);
}
std::stable_sort(
msgs.begin(), msgs.end(),
[](QPair<int, Message> const &a, QPair<int, Message> const &b) {
return QVariant::compare(a.second.params().value("UTC"),
b.second.params().value("UTC")) ==
QPartialOrdering::Greater;
});
auto mw = new MessageWindow(this);
connect(mw, &MessageWindow::finished, this, [this](int) {
refreshInboxCounts();
displayCallActivity();
});
connect(mw, &MessageWindow::deleteMessage, this, [this](int id) {
Inbox inbox(inboxPath());
if (!inbox.open()) {
return;
}
inbox.del(id);
});
connect(mw, &MessageWindow::replyMessage, this,
[this, mw](const QString &text) {
addMessageText(text, true, true);
refreshInboxCounts();
displayCallActivity();
mw->close();
});
mw->setCall(selectedCall);
mw->populateMessages(msgs);
mw->show();
});
auto historyAction =
new QAction(QString("Show Message Inbox..."), ui->tableWidgetCalls);
connect(historyAction, &QAction::triggered, ui->actionShow_Message_Inbox,
&QAction::trigger);
auto localMessageAction =
new QAction(QString("Store Message..."), ui->tableWidgetCalls);
connect(localMessageAction, &QAction::triggered, this, [this]() {
QString selectedCall = callsignSelected();
if (selectedCall.isEmpty()) {
return;
}
auto m = new MessageReplyDialog(this);
m->setWindowTitle("Message");
m->setLabel(
QString("Store this message locally for %1:").arg(selectedCall));
if (m->exec() != QMessageBox::Accepted) {
return;
}
CommandDetail d = {};
d.cmd = " MSG ";
d.to = selectedCall;
d.from = m_config.my_callsign();
d.relayPath = d.from;
d.text = m->textValue();
d.utcTimestamp = DriftingDateTime::currentDateTimeUtc();
d.submode = m_nSubMode;
addCommandToStorage("STORE", d);
});
// Disable default header mouseover and click behaviors, they are confusing
// to users because they give the appearance of allowing sorting by header
// clicks, which is not actually implemented
ui->tableWidgetCalls->horizontalHeader()->setHighlightSections(false);
ui->tableWidgetCalls->horizontalHeader()->setSectionsClickable(false);
ui->tableWidgetCalls->horizontalHeader()->setContextMenuPolicy(
Qt::CustomContextMenu);
connect(
ui->tableWidgetCalls->horizontalHeader(),
&QHeaderView::customContextMenuRequested, this,
[this](QPoint const &point) {
QMenu *menu = new QMenu(ui->tableWidgetCalls);
QMenu *sortByMenu = menu->addMenu("Sort By...");
buildCallActivitySortByMenu(sortByMenu);
QMenu *showColumnsMenu = menu->addMenu("Show Columns...");
buildShowColumnsMenu(showColumnsMenu, "call");
menu->popup(
ui->tableWidgetCalls->horizontalHeader()->mapToGlobal(point));
});
ui->tableWidgetCalls->setContextMenuPolicy(Qt::CustomContextMenu);
connect(
ui->tableWidgetCalls, &QTableWidget::customContextMenuRequested, this,
[this, logAction, historyAction, localMessageAction, clearAction4,
clearActionAll, addStation, removeStation](QPoint const &point) {
QMenu *menu = new QMenu(ui->tableWidgetCalls);
// clear the selection of the call widget on right click
// but only if the table has rows.
if (ui->tableWidgetCalls->rowAt(point.y()) != -1) {
ui->tableWidgetRXAll->selectionModel()->clearSelection();
}
QString selectedCall = callsignSelected();
bool isAllCall = isAllCallIncluded(selectedCall);
// bool isGroupCall = isGroupCallIncluded(selectedCall);
bool missingCallsign = selectedCall.isEmpty();
if (!missingCallsign && !isAllCall) {
int selectedOffset = m_callActivity[selectedCall].offset;
if (selectedOffset != -1) {
auto qsyAction = menu->addAction(
QString("Jump to %1Hz").arg(selectedOffset));
connect(qsyAction, &QAction::triggered, this,
[this, selectedOffset]() {
setFreqOffsetForRestore(selectedOffset, false);
});
if (m_wideGraph->filterEnabled()) {
auto filterQsyAction =
menu->addAction(QString("Center filter at %1Hz")
.arg(selectedOffset));
connect(filterQsyAction, &QAction::triggered, this,
[this, selectedOffset]() {
m_wideGraph->setFilterCenter(
selectedOffset);
});
}
int submode = m_callActivity[selectedCall].submode;
auto speed = JS8::Submode::name(submode);
if (submode != m_nSubMode) {
auto qrqAction =
menu->addAction(QString("Jump to %1%2 speed")
.arg(speed.left(1))
.arg(speed.mid(1).toLower()));
connect(qrqAction, &QAction::triggered, this,
[this, submode]() { setSubmode(submode); });
}
int tdrift =
-int(m_callActivity[selectedCall].tdrift * 1000);
auto qtrAction = menu->addAction(
QString("Jump to %1 ms time drift").arg(tdrift));
connect(qtrAction, &QAction::triggered, this,
[this, tdrift]() { setDrift(tdrift); });
menu->addSeparator();
}
}
menu->addAction(logAction);
logAction->setDisabled(missingCallsign || isAllCall);
menu->addAction(historyAction);
historyAction->setDisabled(missingCallsign || isAllCall ||
!hasMessageHistory(selectedCall));
menu->addAction(localMessageAction);
localMessageAction->setDisabled(missingCallsign || isAllCall);
menu->addSeparator();
auto savedMenu = menu->addMenu("Saved Messages...");
buildSavedMessagesMenu(savedMenu);
auto directedMenu =
menu->addMenu(QString("Directed to %1...").arg(selectedCall));
directedMenu->setDisabled(missingCallsign);
buildQueryMenu(directedMenu, selectedCall);
auto relayAction = buildRelayAction(selectedCall);
relayAction->setText(QString("Relay via %1...").arg(selectedCall));
relayAction->setDisabled(missingCallsign || isAllCall);
menu->addActions({relayAction});
auto deselect =
menu->addAction(QString("Deselect %1").arg(selectedCall));
deselect->setDisabled(missingCallsign);
connect(deselect, &QAction::triggered, this, [this]() {
ui->tableWidgetRXAll->clearSelection();
ui->tableWidgetCalls->clearSelection();
});
menu->addSeparator();
menu->addAction(addStation);
removeStation->setDisabled(missingCallsign);
removeStation->setText(selectedCall.startsWith("@")
? "Remove This Group"
: "Remove This Station");
menu->addAction(removeStation);
menu->addSeparator();
menu->addAction(clearAction4);
menu->addAction(clearActionAll);
menu->popup(ui->tableWidgetCalls->mapToGlobal(point));
});
connect(ui->tableWidgetRXAll->selectionModel(),
&QItemSelectionModel::selectionChanged, this,
&UI_Constructor::tableSelectionChanged);
connect(ui->tableWidgetCalls->selectionModel(),
&QItemSelectionModel::selectionChanged, this,
&UI_Constructor::tableSelectionChanged);
auto p = ui->tableWidgetRXAll->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight,
p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetRXAll->setPalette(p);
p = ui->tableWidgetCalls->palette();
p.setColor(QPalette::Inactive, QPalette::Highlight,
p.color(QPalette::Active, QPalette::Highlight));
ui->tableWidgetCalls->setPalette(p);
ui->hbMacroButton->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->hbMacroButton, &QPushButton::customContextMenuRequested, this,
[this](QPoint const &point) {
QMenu *menu = new QMenu(ui->hbMacroButton);
buildHeartbeatMenu(menu);
menu->popup(ui->hbMacroButton->mapToGlobal(point));
});
ui->cqMacroButton->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->cqMacroButton, &QPushButton::customContextMenuRequested, this,
[this](QPoint const &point) {
QMenu *menu = new QMenu(ui->cqMacroButton);
buildCQMenu(menu);
menu->popup(ui->cqMacroButton->mapToGlobal(point));
});
// Don't block heartbeat's first run...
m_lastTxStartTime = DriftingDateTime::currentDateTimeUtc().addSecs(-300);
// But do block the decoder's first run until 50% through next transmit
// period
m_lastTxStopTime = nextTransmitCycle().addSecs(-m_TRperiod / 2);
int width = 75;
/*
QList<QPushButton*> btns;
foreach(auto child, ui->buttonGrid->children()){
if(!child->isWidgetType()){
continue;
}
if(!child->objectName().contains("Button")){
continue;
}
auto b = qobject_cast<QPushButton*>(child);
width = qMax(width, b->geometry().width());
btns.append(b);
}
*/
foreach (auto child, ui->buttonGrid->children()) {
if (!child->isWidgetType()) {
continue;
}
if (!child->objectName().contains("Button")) {
continue;
}
auto b = qobject_cast<QPushButton *>(child);
b->setCursor(QCursor(Qt::PointingHandCursor));
}
auto buttonLayout = ui->buttonGrid->layout();
auto gridButtonLayout = qobject_cast<QGridLayout *>(buttonLayout);
gridButtonLayout->setColumnMinimumWidth(0, width);
gridButtonLayout->setColumnMinimumWidth(1, width);
gridButtonLayout->setColumnMinimumWidth(2, width);
gridButtonLayout->setColumnStretch(0, 1);
gridButtonLayout->setColumnStretch(1, 1);
gridButtonLayout->setColumnStretch(2, 1);
// dial up and down buttons sizes
ui->dialFreqUpButton->setFixedSize(30, 24);
ui->dialFreqDownButton->setFixedSize(30, 24);
// Prepare spotting configuration...
prepareApi();
prepareSpotting();
displayActivity(true);
m_txTextDirtyDebounce.setSingleShot(true);
connect(&m_txTextDirtyDebounce, &QTimer::timeout, this,
&UI_Constructor::refreshTextDisplay);
qCDebug(mainwindow_js8)
<< "Main window constructor has done all connect (aka plumbing) work.";
m_TxDelay = m_config.txDelay();
m_hb_loop->onTxDelayChange(llround(m_TxDelay * 1000.0));
m_cq_loop->onTxDelayChange(llround(m_TxDelay * 1000.0));
m_hb_loop->onPlumbingCompleted();
m_cq_loop->onPlumbingCompleted();
DriftingDateTimeSingleton::getSingleton().onPlumbingCompleted();
qCDebug(mainwindow_js8)
<< "Initialization with onPlumbingCompleted() has completed.";
QTimer::singleShot(500, this, &UI_Constructor::initializeDummyData);
QTimer::singleShot(500, this, &UI_Constructor::initializeGroupMessage);
// this must be the last statement of constructor
if (!m_valid)
throw std::runtime_error{"Fatal initialization exception"};
}