mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
1452 lines
60 KiB
C++
1452 lines
60 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"
|
|
#include "JS8_UI/styles.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_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);
|
|
ui->frame->setStyleSheet(logFrameStyle());
|
|
ui->logWidget->setStyleSheet(Styles::LogWidgetStyle);
|
|
ui->dialFreqUpButton->setStyleSheet(Styles::DialFreqUpDownButtonStyle);
|
|
ui->dialFreqDownButton->setStyleSheet(Styles::DialFreqUpDownButtonStyle);
|
|
ui->labCallsign->setStyleSheet(Styles::LabCallsignStyle);
|
|
ui->labUTC->setStyleSheet(Styles::LabUTCStyle);
|
|
ui->buttonGrid->setStyleSheet(Styles::ButtonGridStyle);
|
|
ui->monitorTxButton->setStyleSheet(Styles::MonitorTxButtonStyle);
|
|
ui->monitorButton->setStyleSheet(Styles::MonitorButtonStyle);
|
|
ui->logQSOButton->setStyleSheet(Styles::LogQSOButtonStyle);
|
|
ui->tuneButton->setStyleSheet(Styles::TuneButtonStyle);
|
|
ui->modeButton->setStyleSheet(Styles::ModeButtonStyle);
|
|
ui->spotButton->setStyleSheet(Styles::SpotButtonStyle);
|
|
|
|
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](QString const &fromCall, QString const &toCall,
|
|
QString const &messageId) {
|
|
emit aprsClientEnqueueAck(fromCall, toCall, messageId);
|
|
},
|
|
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::aprsClientEnqueueAck, m_aprsClient,
|
|
&APRSISClient::enqueueMessageAck);
|
|
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());
|
|
|
|
// update pill enabled state, colors, and rehighlight compose box
|
|
applyPillSettings();
|
|
ui->extFreeTextMsgEdit->highlight();
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Apply initial pill settings (colors_changed is not emitted at startup)
|
|
applyPillSettings();
|
|
|
|
setWindowTitle(program_title());
|
|
buildColumnLabelMap();
|
|
|
|
// Hook up working frequencies.
|
|
|
|
ui->currentFreq->setCursor(QCursor(Qt::PointingHandCursor));
|
|
ui->currentFreq->setText("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));
|
|
|
|
freqOffsetWidget = new Styles::OffsetSliderWidget(nullptr);
|
|
QLayout *parentLayout = ui->labDialFreqOffset->parentWidget()->layout();
|
|
if (auto *vbox = qobject_cast<QVBoxLayout *>(parentLayout)) {
|
|
int index = vbox->indexOf(ui->labDialFreqOffset);
|
|
vbox->removeWidget(ui->labDialFreqOffset);
|
|
ui->labDialFreqOffset->hide();
|
|
vbox->insertWidget(index, freqOffsetWidget);
|
|
}
|
|
freqOffsetWidget->setOnValueChanged([this](int val) {
|
|
changeFreq(val);
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
|
|
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]() {
|
|
changeFreq(selectedOffset);
|
|
});
|
|
|
|
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();
|
|
messagePanel_->setCall("%");
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
auto blockStation =
|
|
new QAction(QString("Block This Station"), ui->tableWidgetCalls);
|
|
connect(blockStation, &QAction::triggered, this, [this]() {
|
|
QString selectedCall = callsignSelected();
|
|
if (selectedCall.isEmpty() || selectedCall.startsWith("@")) {
|
|
return;
|
|
}
|
|
|
|
m_config.add_to_rx_callsign_blocklist(selectedCall);
|
|
|
|
m_rxBlockedOffsets.clear();
|
|
displayActivity(true);
|
|
});
|
|
|
|
connect(ui->actionShow_Message_Inbox, &QAction::toggled, this,
|
|
[this](bool checked) {
|
|
|
|
if (checked) {
|
|
ensureMessageDock();
|
|
messageDock_->show();
|
|
messageDock_->raise();
|
|
|
|
/*
|
|
* Disable the automatic selected call filter now that the inbox lives
|
|
* in a dockable panel
|
|
QString selectedCall = callsignSelected();
|
|
if (selectedCall.isEmpty()) selectedCall = "%";
|
|
messagePanel_->setCall(selectedCall);
|
|
*/
|
|
messagePanel_->setCall("%");
|
|
} else {
|
|
if (messageDock_) {
|
|
// pick ONE: hide or close; for docks, hide is simplest
|
|
messageDock_->hide();
|
|
}
|
|
}
|
|
});
|
|
|
|
// When a message is added to the inbox, refresh the message panel
|
|
connect(this, &UI_Constructor::messageAdded, messagePanel_, &MessagePanel::refresh);
|
|
|
|
auto historyAction =
|
|
new QAction(QString("Show Message Inbox..."), ui->tableWidgetCalls);
|
|
connect(historyAction, &QAction::triggered, this, [this]() {
|
|
ensureMessageDock();
|
|
messageDock_->show();
|
|
messageDock_->raise();
|
|
|
|
QString selectedCall = callsignSelected();
|
|
messagePanel_->setCall(selectedCall);
|
|
});
|
|
|
|
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, blockStation](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]() {
|
|
changeFreq(selectedOffset);
|
|
});
|
|
|
|
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();
|
|
messagePanel_->setCall("%");
|
|
});
|
|
|
|
menu->addSeparator();
|
|
|
|
menu->addAction(addStation);
|
|
removeStation->setDisabled(missingCallsign);
|
|
removeStation->setText(selectedCall.startsWith("@")
|
|
? "Remove This Group"
|
|
: "Remove This Station");
|
|
menu->addAction(removeStation);
|
|
|
|
blockStation->setDisabled(missingCallsign || isAllCall ||
|
|
selectedCall.startsWith("@"));
|
|
|
|
menu->addAction(blockStation);
|
|
|
|
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"};
|
|
}
|