mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
Move OmniRig to deprecated group, scheduled for deprecation due to being no longer supported by Windows 11 and Qt6.9.3 Schedule OmniRig to removed from the code in July 2026 Noted in the doxy file description
1431 lines
59 KiB
C++
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} {
|
|
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"};
|
|
}
|