js8call/JS8_UI/Configuration.cpp
Chris-AC9KH 1b9a2f8a11 Add right click context menu item to block a station
Add unicode red "x" to signify a blocked call or station
2026-05-15 21:36:11 -05:00

5202 lines
204 KiB
C++

/**
* @file Configuration.cpp
* @brief Implementation of the Configuration dialog for the UI
*/
//
// Read me!
//
// This file defines a configuration dialog with the user. The general
// strategy is to expose agreed configuration parameters via a custom
// interface (See Configuration.h). The state exposed through this
// public interface reflects stored or derived data in the
// Configuration::impl object. The Configuration::impl structure is
// an implementation of the PIMPL (a.k.a. Cheshire Cat or compilation
// firewall) implementation hiding idiom that allows internal state to
// be completely removed from the public interface.
//
// There is a secondary level of parameter storage which reflects
// current settings UI state, these parameters are not copied to the
// state store that the public interface exposes until the
// Configuration:impl::accept() operation is successful. The accept()
// operation is tied to the settings OK button. The normal and most
// convenient place to store this intermediate settings UI state is in
// the data models of the UI controls, if that is not convenient then
// separate member variables must be used to store that state. It is
// important for the user experience that no publicly visible settings
// are changed while the settings UI are changed i.e. all settings
// changes must be deferred until the "OK" button is
// clicked. Conversely, all changes must be discarded if the settings
// UI "Cancel" button is clicked.
//
// There is a complication related to the radio interface since the
// this module offers the facility to test the radio interface. This
// test means that the public visibility to the radio being tested
// must be changed. To maintain the illusion of deferring changes
// until they are accepted, the original radio related settings are
// stored upon showing the UI and restored if the UI is dismissed by
// canceling.
//
// It should be noted that the settings UI lives as long as the
// application client that uses it does. It is simply shown and hidden
// as it is needed rather than creating it on demand. This strategy
// saves a lot of expensive UI drawing at the expense of a little
// storage and gives a convenient place to deliver settings values
// from.
//
// Here is an overview of the high level flow of this module:
//
// 1) On construction the initial environment is initialized and
// initial values for settings are read from the QSettings
// database. At this point default values for any new settings are
// established by providing a default value to the QSettings value
// queries. This should be the only place where a hard coded value for
// a settings item is defined. Any remaining one-time UI
// initialization is also done. At the end of the constructor a method
// initialize_models() is called to load the UI with the current
// settings values.
//
// 2) When the settings UI is displayed by a client calling the exec()
// operation, only temporary state need be stored as the UI state will
// already mirror the publicly visible settings state.
//
// 3) As the user makes changes to the settings UI only validation
// need be carried out since the UI control data models are used as
// the temporary store of unconfirmed settings. As some settings will
// depend on each other a validate() operation is available, this
// operation implements a check of any complex multi-field values.
//
// 4) If the user discards the settings changes by dismissing the UI
// with the "Cancel" button; the reject() operation is called. The
// reject() operation calls initialize_models() which will revert all
// the UI visible state to the values as at the initial exec()
// operation. No changes are moved into the data fields in
// Configuration::impl that reflect the settings state published by
// the public interface (Configuration.h).
//
// 5) If the user accepts the settings changes by dismissing the UI
// with the "OK" button; the accept() operation is called which calls
// the validate() operation again and, if it passes, the fields that
// are used to deliver the settings state are updated from the UI
// control models or other temporary state variables. At the end of
// the accept() operation, just before hiding the UI and returning
// control to the caller; the new settings values are stored into the
// settings database by a call to the write_settings() operation, thus
// ensuring that settings changes are saved even if the application
// crashes or is subsequently killed.
//
// 6) On destruction, which only happens when the application
// terminates, the settings are saved to the settings database by
// calling the write_settings() operation. This is largely redundant
// but is still done to save the default values of any new settings on
// an initial run.
//
// To add a new setting:
//
// 1) Update the UI with the new widget to view and change the value.
//
// 2) Add a member to Configuration::impl to store the accepted
// setting state. If the setting state is dynamic; add a new signal to
// broadcast the setting value.
//
// 3) Add a query method to the public interface (Configuration.h)
// to access the new setting value. If the settings is dynamic; this
// step is optional since value changes will be broadcast via a
// signal.
//
// 4) Add a forwarding operation to implement the new query (3) above.
//
// 5) Add a settings read call to read_settings() with a sensible
// default value. If the setting value is dynamic, add a signal emit
// call to broadcast the setting value change.
//
// 6) Add code to initialize_models() to load the widget control's
// data model with the current value.
//
// 7) If there is no convenient data model field, add a data member to
// store the proposed new value. Ensure this member has a valid value
// on exit from read_settings().
//
// 8) Add any required inter-field validation to the validate()
// operation.
//
// 9) Add code to the accept() operation to extract the setting value
// from the widget control data model and load it into the
// Configuration::impl member that reflects the publicly visible
// setting state. If the setting value is dynamic; add a signal emit
// call to broadcast any changed state of the setting.
//
// 10) Add a settings write call to save the setting value to the
// settings database.
//
#include "Configuration.h"
#include "JS8_Include/Maidenhead.h"
#include "JS8_Include/SettingsGroup.h"
#include "JS8_Include/pimpl_impl.h"
#include "JS8_Main/Bands.h"
#include "JS8_Main/CallsignValidator.h"
#include "JS8_Main/CandidateKeyFilter.h"
#include "JS8_Main/ForeignKeyDelegate.h"
#include "JS8_Main/FrequencyLineEdit.h"
#include "JS8_Main/FrequencyList.h"
#include "JS8_Main/IARURegions.h"
#include "JS8_Main/JS8MessageBox.h"
#include "JS8_Main/MetaDataRegistry.h"
#include "JS8_Main/Modes.h"
#include "JS8_Main/StationList.h"
#include "JS8_Main/Varicode.h"
#include "JS8_Main/qt_helpers.h"
#include "JS8_Network/NetworkServerLookup.h"
#include "JS8_Transceiver/Transceiver.h"
#include "JS8_Transceiver/TransceiverFactory.h"
#include "JS8_Widgets/CheckableItemComboBox.h"
#include "JS8_Widgets/LazyFillComboBox.h"
#include "ui_Configuration.h"
#include <QAction>
#include <QApplication>
#include <QAudioDevice>
#include <QAudioInput>
#include <QClipboard>
#include <QColorDialog>
#include <QDateTime>
#include <QDateTimeEdit>
#include <QDialog>
#include <QDir>
#include <QFileDialog>
#include <QFont>
#include <QFontDatabase>
#include <QFontDialog>
#include <QFormLayout>
#include <QIntValidator>
#include <QLibraryInfo>
#include <QLineEdit>
#include <QList>
#include <QLoggingCategory>
#include <QMediaDevices>
#include <QMetaType>
#include <QNetworkInterface>
#include <QProcess>
#include <QRegularExpressionValidator>
#include <QScopedPointer>
#include <QSerialPortInfo>
#include <QSettings>
#include <QSoundEffect>
#include <QStandardItemModel>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QStringListModel>
#include <QSysInfo>
#include <QTemporaryFile>
#include <QTextStream>
#include <QThread>
#include <QTimeZone>
#include <QTimer>
#include <algorithm>
#include <cmath>
#include <iterator>
#include <limits>
#include <stdexcept>
#include "moc_Configuration.cpp"
namespace {
const QRegularExpression message_alphabet{"[^\\x00-\\x1F]*"};
// Magic numbers for file validation
constexpr quint32 qrg_magic{0xadbccbdb};
constexpr quint32 qrg_version{103}; // M.mm
// Bump this versioned key every time we need to "reset" our working
// frequencies...
const char *const versionedFrequenciesSettingsKey =
"FrequenciesForRegionModes_v3";
const char *const v2FrequenciesSettingsKey =
"FrequenciesForRegionModes_01";
}// namespace
//
// Dialog to get a new Frequency item
//
class FrequencyDialog final : public QDialog {
public:
using Item = FrequencyList_v3::Item;
explicit FrequencyDialog(IARURegions *regions_model, Modes *modes_model,
QWidget *parent = nullptr)
: QDialog{parent} {
setWindowTitle(QApplication::applicationName() + " - " +
tr("Add Frequency"));
region_combo_box_.setModel(regions_model);
mode_combo_box_.setModel(modes_model);
auto form_layout = new QFormLayout();
form_layout->addRow(tr("IARU &Region:"), &region_combo_box_);
form_layout->addRow(tr("&Mode:"), &mode_combo_box_);
form_layout->addRow(tr("&Frequency (MHz):"), &frequency_line_edit_);
form_layout->addRow(tr("&Description:"), &description_line_edit_);
auto main_layout = new QVBoxLayout(this);
main_layout->addLayout(form_layout);
auto button_box = new QDialogButtonBox{QDialogButtonBox::Ok |
QDialogButtonBox::Cancel};
main_layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, this,
&FrequencyDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this,
&FrequencyDialog::reject);
}
Item item() const {
return {frequency_line_edit_.frequency(),
Modes::value(mode_combo_box_.currentText()),
IARURegions::value(region_combo_box_.currentText()),
description_line_edit_.text().trimmed()
};
}
private:
QComboBox region_combo_box_;
QComboBox mode_combo_box_;
FrequencyLineEdit frequency_line_edit_;
QLineEdit description_line_edit_;
};
//
// Dialog to get a new Station item
//
class StationDialog final : public QDialog {
public:
explicit StationDialog(StationList const *stations, Bands *bands,
QWidget *parent = nullptr)
: QDialog{parent},
filtered_bands_{new CandidateKeyFilter{bands, stations, 0, 0}},
all_bands_{bands} {
setWindowTitle(QApplication::applicationName() + " - " +
tr("Add Schedule"));
band_.setModel(filtered_bands_.data());
switch_at_.setTimeZone(QTimeZone::utc());
switch_at_.setDisplayFormat("hh:mm");
switch_until_.setTimeZone(QTimeZone::utc());
switch_until_.setDisplayFormat("hh:mm");
auto form_layout = new QFormLayout();
form_layout->addRow(tr("&Frequency (MHz):"), &freq_);
form_layout->addRow(tr("&Switch at (UTC):"), &switch_at_);
form_layout->addRow(tr("&Until (UTC):"), &switch_until_);
form_layout->addRow(tr("&Description:"), &description_);
auto main_layout = new QVBoxLayout(this);
main_layout->addLayout(form_layout);
auto button_box = new QDialogButtonBox{QDialogButtonBox::Ok |
QDialogButtonBox::Cancel};
main_layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, this,
&StationDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this,
&StationDialog::reject);
}
StationList::Station station() const {
auto band = all_bands_->find(freq_.frequency());
auto a =
QDateTime(QDate(2000, 1, 1), switch_at_.time(), QTimeZone::utc());
auto b = QDateTime(QDate(2000, 1, 1), switch_until_.time(),
QTimeZone::utc());
return {band, freq_.frequency(), a, b, description_.text()};
}
int exec() override {
filtered_bands_->set_active_key();
return QDialog::exec();
}
private:
QScopedPointer<CandidateKeyFilter> filtered_bands_;
Bands *all_bands_;
QComboBox band_;
FrequencyLineEdit freq_;
QTimeEdit switch_at_;
QTimeEdit switch_until_;
QLineEdit description_;
};
class RearrangableMacrosModel : public QStringListModel {
public:
Qt::ItemFlags flags(QModelIndex const &index) const override {
auto flags = QStringListModel::flags(index);
if (index.isValid()) {
// disallow drop onto existing items
flags &= ~Qt::ItemIsDropEnabled;
}
return flags;
}
};
//
// Class MessageItemDelegate
//
// Item delegate for message entry such as free text message macros.
//
class MessageItemDelegate final : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QWidget *createEditor(QWidget *parent,
QStyleOptionViewItem const & /* option*/
,
QModelIndex const & /* index */
) const override {
auto editor = new QLineEdit{parent};
editor->setFrame(false);
editor->setValidator(
new QRegularExpressionValidator{message_alphabet, editor});
return editor;
}
};
// Internal implementation of the Configuration class.
class Configuration::impl final : public QDialog {
Q_OBJECT;
public:
using FrequencyDelta = Radio::FrequencyDelta;
using port_type = Configuration::port_type;
explicit impl(Configuration *self, QDir const &temp_directory,
QSettings *settings, QWidget *parent);
~impl();
bool have_rig();
void transceiver_frequency(Frequency);
void transceiver_tx_frequency(Frequency);
void transceiver_mode(MODE);
void transceiver_ptt(bool);
void sync_transceiver(bool force_signal);
Q_SLOT int exec() override;
Q_SLOT void accept() override;
Q_SLOT void reject() override;
Q_SLOT void done(int) override;
private:
typedef QList<QAudioDevice> AudioDevices;
struct AudioBinding
{
QAudioDevice::Mode mode;
QComboBox* device_combo = nullptr;
QComboBox* channel_combo = nullptr; // nullptr for notifications (no channel UI)
bool allow_both = false; // true for output, false for input
bool allow_monitor_inputs = false; // only relevant for input on Linux
// Settings keys
const char* key_id = nullptr;
const char* key_name = nullptr; // legacy/migration hint
const char* key_channel = nullptr; // nullptr for notifications
};
void read_settings();
void write_settings();
void find_audio_devices();
void resolve_audio_binding(AudioBinding const& b,
QAudioDevice& next_device,
AudioDevice::Channel* next_channel);
QAudioDevice find_audio_device(QAudioDevice::Mode mode,
QComboBox *combo_box,
QString const& saved_id,
QString const& saved_name,
bool allow_monitors);
void load_audio_devices(QAudioDevice::Mode mode,
QComboBox *combo_box,
QAudioDevice *device,
bool show_monitors = true);
void update_audio_channels(QComboBox const *, QComboBox const *, int, bool);
void load_network_interfaces(CheckableItemComboBox *, QStringList current);
static QAction *attachRequiredIndicator(
QLineEdit *edit, const QString &toolTip = QStringLiteral("Required"));
QStringList get_selected_network_interfaces(CheckableItemComboBox *);
static QString dev_id(QAudioDevice const &d);
static bool is_monitor(QAudioDevice const &d);
static bool looks_like_capture(QAudioDevice const &d);
static QString id_tail(QAudioDevice const &d);
static QString make_label(QString const &base, QAudioDevice const &d,
int dupCount);
void run_diagnostics();
QString build_diagnostics_text() const;
static QString run_cmd(QString const &program, QStringList const &args,
int timeout_ms);
static void append_env(QTextStream &ts, char const *key);
static QString dump_devices(QString const &title,
QList<QAudioDevice> const &devices);
void find_tab(QWidget *);
void initialize_models();
bool split_mode() const {
return (WSJT_RIG_NONE_CAN_SPLIT || !rig_is_dummy_) &&
(rig_params_.split_mode != TransceiverFactory::split_mode_none);
}
void set_cached_mode();
bool open_rig(bool force = false);
// bool set_mode ();
void close_rig();
TransceiverFactory::ParameterPack gather_rig_data() const;
void enumerate_rigs();
void set_rig_invariants();
bool validate();
void fill_port_combo_box(QComboBox *);
Frequency apply_calibration(Frequency) const;
Frequency remove_calibration(Frequency) const;
void updatePillPreview(QLabel *label, const QColor &bg, const QColor &fg);
void choosePillPreviewColor(QColor &target, QLabel *label,
const QColor &bg, const QColor &fg,
const QString &title);
void delete_frequencies();
void load_frequencies();
void merge_frequencies();
void save_frequencies();
void reset_frequencies();
void insert_frequency();
FrequencyList_v3::FrequencyItems read_frequencies_file(QString const &);
void delete_stations();
void insert_station();
Q_SLOT void on_psk_reporter_check_box_toggled(bool checked);
Q_SLOT void on_enable_aprs_spotting_check_box_toggled(bool checked);
Q_SLOT void on_enable_aprs_relay_check_box_toggled(bool checked);
Q_SLOT void on_notifications_check_box_toggled(bool checked);
Q_SLOT void on_font_push_button_clicked();
Q_SLOT void on_style_push_button_clicked();
Q_SLOT void on_tableFontButton_clicked();
Q_SLOT void on_PTT_port_combo_box_activated(int);
Q_SLOT void on_CAT_port_combo_box_activated(int);
Q_SLOT void on_CAT_serial_baud_combo_box_currentIndexChanged(int);
Q_SLOT void on_CAT_data_bits_button_group_buttonClicked(QAbstractButton *);
Q_SLOT void on_CAT_stop_bits_button_group_buttonClicked(QAbstractButton *);
Q_SLOT void on_CAT_handshake_button_group_buttonClicked(QAbstractButton *);
Q_SLOT void on_CAT_poll_interval_spin_box_valueChanged(int);
Q_SLOT void on_split_mode_button_group_buttonClicked(QAbstractButton *);
Q_SLOT void on_test_CAT_push_button_clicked();
Q_SLOT void on_test_PTT_push_button_clicked(bool checked);
Q_SLOT void on_force_DTR_combo_box_currentIndexChanged(int);
Q_SLOT void on_force_RTS_combo_box_currentIndexChanged(int);
Q_SLOT void on_rig_combo_box_currentIndexChanged(int);
Q_SLOT void on_add_macro_push_button_clicked(bool = false);
Q_SLOT void on_delete_macro_push_button_clicked(bool = false);
Q_SLOT void on_PTT_method_button_group_buttonClicked(QAbstractButton *);
Q_SLOT void on_groups_line_edit_textChanged(QString const &);
Q_SLOT void on_info_message_line_edit_textChanged(QString const &);
Q_SLOT void on_cq_message_line_edit_textChanged(QString const &);
Q_SLOT void on_reply_message_line_edit_textChanged(QString const &);
Q_SLOT void on_add_macro_line_edit_editingFinished();
Q_SLOT void delete_macro();
void delete_selected_macros(QModelIndexList);
Q_SLOT void on_save_path_select_push_button_clicked(bool);
Q_SLOT void on_calibration_intercept_spin_box_valueChanged(double);
Q_SLOT void on_calibration_slope_ppm_spin_box_valueChanged(double);
Q_SLOT void handle_transceiver_update(TransceiverState const &,
unsigned sequence_number);
Q_SLOT void handle_transceiver_failure(QString const &reason);
Q_SLOT void on_cqMessagesButton_clicked();
Q_SLOT void on_primaryHighlightButton_clicked();
Q_SLOT void on_secondaryHighlightButton_clicked();
Q_SLOT void on_pbMyCall_clicked();
Q_SLOT void on_tableBackgroundButton_clicked();
Q_SLOT void on_tableSelectedRowBackgroundButton_clicked();
Q_SLOT void on_tableForegroundButton_clicked();
Q_SLOT void on_rxBackgroundButton_clicked();
Q_SLOT void on_rxForegroundButton_clicked();
Q_SLOT void on_rxFontButton_clicked();
Q_SLOT void on_composeBackgroundButton_clicked();
Q_SLOT void on_composeForegroundButton_clicked();
Q_SLOT void on_composeFontButton_clicked();
Q_SLOT void on_txForegroundButton_clicked();
Q_SLOT void on_txFontButton_clicked();
Q_SLOT void on_pillRecipientBgButton_clicked();
Q_SLOT void on_pillRecipientFgButton_clicked();
Q_SLOT void on_pillCommandBgButton_clicked();
Q_SLOT void on_pillCommandFgButton_clicked();
Q_SLOT void on_pillGroupBgButton_clicked();
Q_SLOT void on_pillGroupFgButton_clicked();
Q_SLOT void on_pillSenderBgButton_clicked();
Q_SLOT void on_pillSenderFgButton_clicked();
// typenames used as arguments must match registered type names :(
Q_SIGNAL void start_transceiver(unsigned seqeunce_number) const;
Q_SIGNAL void set_transceiver(Transceiver::TransceiverState const &,
unsigned sequence_number) const;
Q_SIGNAL void stop_transceiver() const;
Configuration *const self_; // back pointer to public interface
QThread *transceiver_thread_;
TransceiverFactory transceiver_factory_;
QList<QMetaObject::Connection> rig_connections_;
QScopedPointer<Ui::configuration_dialog> ui_;
QSettings *settings_;
QDir temp_dir_;
QDir writeable_data_dir_;
QDir default_save_directory_;
QDir save_directory_;
QFont font_;
QFont next_font_;
QFont table_font_;
QFont next_table_font_;
QFont rx_text_font_;
QFont next_rx_text_font_;
QFont tx_text_font_;
QFont next_tx_text_font_;
QFont compose_text_font_;
QFont next_compose_text_font_;
bool restart_sound_input_device_;
bool restart_sound_output_device_;
bool restart_notification_sound_output_device_;
bool enable_notifications_;
QMap<QString, bool> notifications_enabled_;
QMap<QString, QString> notifications_paths_;
QStringListModel macros_;
RearrangableMacrosModel next_macros_;
QAction *macro_delete_action_;
Bands bands_;
IARURegions regions_;
IARURegions::Region region_;
Modes modes_;
FrequencyList_v3 frequencies_;
FrequencyList_v3 next_frequencies_;
StationList stations_;
StationList next_stations_;
QAction *frequency_delete_action_;
QAction *frequency_insert_action_;
QAction *load_frequencies_action_;
QAction *save_frequencies_action_;
QAction *merge_frequencies_action_;
QAction *reset_frequencies_action_;
FrequencyDialog *frequency_dialog_;
QAction *station_hop_action_;
QAction *station_delete_action_;
QAction *station_insert_action_;
StationDialog *station_dialog_;
TransceiverFactory::ParameterPack rig_params_;
TransceiverFactory::ParameterPack saved_rig_params_;
TransceiverFactory::Capabilities::PortType last_port_type_;
bool rig_is_dummy_;
bool rig_active_;
bool have_rig_;
bool rig_changed_;
TransceiverState cached_rig_state_;
int rig_resolution_; // see Transceiver::resolution signal
CalibrationParams calibration_;
bool frequency_calibration_disabled_; // not persistent
unsigned transceiver_command_number_;
QString dynamic_grid_;
QString dynamic_info_;
QString dynamic_status_;
// configuration fields that we publish
bool auto_switch_bands_;
QString my_callsign_;
QString my_grid_;
QStringList my_groups_;
QStringList auto_whitelist_;
QStringList auto_blacklist_;
QStringList hb_blacklist_;
QStringList spot_blacklist_;
QStringList rx_callsign_blocklist_;
QStringList primary_highlight_words_;
QStringList secondary_highlight_words_;
QString eot_;
QString mfi_;
QString my_info_;
QString my_status_;
QString cq_;
QString hb_;
QString reply_;
int callsign_aging_;
int activity_aging_;
QColor color_primary_highlight_;
QColor next_color_primary_highlight_;
QColor color_secondary_highlight_;
QColor next_color_secondary_highlight_;
QColor color_cq_;
QColor next_color_cq_;
QColor color_mycall_;
QColor next_color_mycall_;
QColor color_table_background_;
QColor next_color_table_background_;
QColor color_table_highlight_;
QColor next_color_table_highlight_;
QColor color_table_foreground_;
QColor next_color_table_foreground_;
QColor color_rx_background_;
QColor next_color_rx_background_;
QColor color_rx_foreground_;
QColor next_color_rx_foreground_;
QColor color_compose_background_;
QColor next_color_compose_background_;
QColor color_compose_foreground_;
QColor next_color_compose_foreground_;
QColor color_tx_foreground_;
QColor next_color_tx_foreground_;
QColor color_DXCC_;
QColor next_color_DXCC_;
QColor color_NewCall_;
QColor next_color_NewCall_;
bool pills_enabled_;
QColor color_pill_recipient_bg_;
QColor next_color_pill_recipient_bg_;
QColor color_pill_recipient_fg_;
QColor next_color_pill_recipient_fg_;
QColor color_pill_command_bg_;
QColor next_color_pill_command_bg_;
QColor color_pill_command_fg_;
QColor next_color_pill_command_fg_;
QColor color_pill_group_bg_;
QColor next_color_pill_group_bg_;
QColor color_pill_group_fg_;
QColor next_color_pill_group_fg_;
QColor color_pill_sender_bg_;
QColor next_color_pill_sender_bg_;
QColor color_pill_sender_fg_;
QColor next_color_pill_sender_fg_;
double txDelay_;
bool write_logs_;
bool reset_activity_;
bool check_for_updates_;
bool tx_qsy_allowed_;
bool spot_to_reporting_networks_;
bool spot_to_aprs_;
bool spot_to_aprs_relay_;
bool transmit_directed_;
bool autoreply_on_at_startup_;
bool autoreply_confirmation_;
bool heartbeat_anywhere_;
bool heartbeat_qso_pause_;
bool heartbeat_ack_snr_;
bool hb_rate_limit_;
bool relay_disabled_;
bool monitor_off_at_startup_;
bool transmit_off_at_startup_;
bool monitor_last_used_;
bool insert_blank_;
bool DXCC_;
bool ppfx_;
bool miles_;
bool hold_ptt_;
bool avoid_forced_identify_;
bool avoid_allcall_;
bool spellcheck_;
int heartbeat_;
int watchdog_;
bool TX_messages_;
bool use_dynamic_info_;
QString opCall_;
QString ptt_command_;
QString aprs_server_name_;
port_type aprs_server_port_;
QString udp_server_name_;
port_type udp_server_port_;
QString tcp_server_name_;
port_type tcp_server_port_;
QString n3fjp_server_name_;
port_type n3fjp_server_port_;
bool broadcast_to_n3fjp_;
QString n1mm_server_name_;
port_type n1mm_server_port_;
bool broadcast_to_n1mm_;
bool accept_udp_requests_;
bool accept_tcp_requests_;
bool udpEnabled_;
bool tcpEnabled_;
int tcpMaxConnections_;
// WSJT-X Protocol settings
bool wsjtx_protocol_enabled_;
QString wsjtx_server_name_;
port_type wsjtx_server_port_;
int wsjtx_TTL_;
bool wsjtx_accept_requests_;
QStringList wsjtx_interface_names_;
QString wsjtx_loopback_interface_name_;
DataMode data_mode_;
bool pwrBandTxMemory_;
bool pwrBandTuneMemory_;
QAudioDevice audio_input_device_;
QAudioDevice next_audio_input_device_;
AudioDevice::Channel audio_input_channel_;
AudioDevice::Channel next_audio_input_channel_;
QAudioDevice audio_output_device_;
QAudioDevice next_audio_output_device_;
AudioDevice::Channel audio_output_channel_;
AudioDevice::Channel next_audio_output_channel_;
QAudioDevice notification_audio_output_device_;
QAudioDevice next_notification_audio_output_device_;
friend class Configuration;
void hop_to_station();
};
#include "Configuration.moc"
Q_DECLARE_LOGGING_CATEGORY(configuration_js8)
// delegate to implementation class
Configuration::Configuration(QDir const &temp_directory, QSettings *settings,
QWidget *parent)
: m_{this, temp_directory, settings, parent} {}
Configuration::~Configuration() {}
QDir Configuration::writeable_data_dir() const {
return m_->writeable_data_dir_;
}
QDir Configuration::temp_dir() const { return m_->temp_dir_; }
void Configuration::select_tab(int index) {
m_->ui_->configuration_tabs->setCurrentIndex(index);
}
int Configuration::exec() { return m_->exec(); }
bool Configuration::is_active() const { return m_->isVisible(); }
QAudioDevice const &Configuration::audio_input_device() const {
return m_->audio_input_device_;
}
AudioDevice::Channel Configuration::audio_input_channel() const {
return m_->audio_input_channel_;
}
QAudioDevice const &Configuration::audio_output_device() const {
return m_->audio_output_device_;
}
AudioDevice::Channel Configuration::audio_output_channel() const {
return m_->audio_output_channel_;
}
QAudioDevice const &Configuration::notification_audio_output_device() const {
return m_->notification_audio_output_device_;
}
bool Configuration::notifications_enabled() const {
return m_->enable_notifications_;
}
QString Configuration::notification_path(const QString &key) const {
if (!m_->enable_notifications_) {
return "";
}
if (!m_->notifications_enabled_.value(key, false)) {
return "";
}
return m_->notifications_paths_.value(key, "");
}
bool Configuration::restart_audio_input() const {
return m_->restart_sound_input_device_;
}
bool Configuration::restart_audio_output() const {
return m_->restart_sound_output_device_;
}
bool Configuration::restart_notification_audio_output() const {
return m_->restart_notification_sound_output_device_;
}
bool Configuration::use_dynamic_grid() const { return m_->use_dynamic_info_; }
QString Configuration::my_callsign() const { return m_->my_callsign_; }
QColor Configuration::color_table_background() const {
return m_->color_table_background_;
}
QColor Configuration::color_table_highlight() const {
return m_->color_table_highlight_;
}
QColor Configuration::color_table_foreground() const {
return m_->color_table_foreground_;
}
QColor Configuration::color_primary_highlight() const {
return m_->color_primary_highlight_;
}
QColor Configuration::color_secondary_highlight() const {
return m_->color_secondary_highlight_;
}
QColor Configuration::color_CQ() const { return m_->color_cq_; }
QColor Configuration::color_MyCall() const { return m_->color_mycall_; }
QColor Configuration::color_rx_background() const {
return m_->color_rx_background_;
}
QColor Configuration::color_rx_foreground() const {
return m_->color_rx_foreground_;
}
QColor Configuration::color_tx_foreground() const {
return m_->color_tx_foreground_;
}
QColor Configuration::color_compose_background() const {
return m_->color_compose_background_;
}
QColor Configuration::color_compose_foreground() const {
return m_->color_compose_foreground_;
}
QColor Configuration::color_DXCC() const { return m_->color_DXCC_; }
QColor Configuration::color_NewCall() const { return m_->color_NewCall_; }
bool Configuration::pills_enabled() const { return m_->pills_enabled_; }
QColor Configuration::color_pill_recipient_bg() const { return m_->color_pill_recipient_bg_; }
QColor Configuration::color_pill_recipient_fg() const { return m_->color_pill_recipient_fg_; }
QColor Configuration::color_pill_command_bg() const { return m_->color_pill_command_bg_; }
QColor Configuration::color_pill_command_fg() const { return m_->color_pill_command_fg_; }
QColor Configuration::color_pill_group_bg() const { return m_->color_pill_group_bg_; }
QColor Configuration::color_pill_group_fg() const { return m_->color_pill_group_fg_; }
QColor Configuration::color_pill_sender_bg() const { return m_->color_pill_sender_bg_; }
QColor Configuration::color_pill_sender_fg() const { return m_->color_pill_sender_fg_; }
QFont Configuration::table_font() const { return m_->table_font_; }
QFont Configuration::text_font() const { return m_->font_; }
QFont Configuration::rx_text_font() const { return m_->rx_text_font_; }
QFont Configuration::tx_text_font() const { return m_->tx_text_font_; }
QFont Configuration::compose_text_font() const {
return m_->compose_text_font_;
}
double Configuration::txDelay() const { return m_->txDelay_; }
bool Configuration::write_logs() const { return m_->write_logs_; }
bool Configuration::reset_activity() const { return m_->reset_activity_; }
bool Configuration::check_for_updates() const { return m_->check_for_updates_; }
bool Configuration::tx_qsy_allowed() const { return m_->tx_qsy_allowed_; }
bool Configuration::spot_to_reporting_networks() const {
// rig must be open and working to spot externally
return is_transceiver_online() && m_->spot_to_reporting_networks_;
}
void Configuration::set_spot_to_reporting_networks(bool spot) {
if (m_->spot_to_reporting_networks_ != spot) {
m_->spot_to_reporting_networks_ = spot;
m_->write_settings();
}
}
bool Configuration::spot_to_aprs() const {
return spot_to_reporting_networks() && m_->spot_to_aprs_;
}
bool Configuration::spot_to_aprs_relay() const {
return spot_to_aprs() && m_->spot_to_aprs_relay_;
}
bool Configuration::transmit_directed() const { return m_->transmit_directed_; }
bool Configuration::autoreply_on_at_startup() const {
// auto-reply cannot be on at startup if the callsign or grid is empty
if (my_callsign().isEmpty() || my_grid().isEmpty()) {
return false;
}
return m_->autoreply_on_at_startup_;
}
bool Configuration::autoreply_confirmation() const {
return m_->autoreply_confirmation_;
}
void Configuration::set_autoreply_confirmation(bool enabled) {
if (m_->autoreply_confirmation_ != enabled) {
m_->autoreply_confirmation_ = enabled;
m_->write_settings();
}
}
bool Configuration::heartbeat_anywhere() const {
return m_->heartbeat_anywhere_;
}
bool Configuration::heartbeat_qso_pause() const {
return m_->heartbeat_qso_pause_;
}
bool Configuration::heartbeat_ack_snr() const {
#if JS8_HB_ACK_SNR_CONFIGURABLE
return m_->heartbeat_ack_snr_;
#else
return true;
#endif
}
bool Configuration::hb_rate_limit() const { return m_->hb_rate_limit_; }
bool Configuration::relay_off() const { return m_->relay_disabled_; }
bool Configuration::monitor_off_at_startup() const {
return m_->monitor_off_at_startup_;
}
bool Configuration::transmit_off_at_startup() const {
return m_->transmit_off_at_startup_;
}
bool Configuration::monitor_last_used() const {
return m_->rig_is_dummy_ || m_->monitor_last_used_;
}
bool Configuration::insert_blank() const { return m_->insert_blank_; }
bool Configuration::DXCC() const { return m_->DXCC_; }
bool Configuration::ppfx() const { return m_->ppfx_; }
bool Configuration::miles() const { return m_->miles_; }
bool Configuration::hold_ptt() const { return m_->hold_ptt_; }
bool Configuration::avoid_forced_identify() const {
return m_->avoid_forced_identify_;
}
bool Configuration::avoid_allcall() const { return m_->avoid_allcall_; }
void Configuration::set_avoid_allcall(bool avoid) {
if (m_->avoid_allcall_ != avoid) {
m_->avoid_allcall_ = avoid;
m_->write_settings();
}
}
bool Configuration::spellcheck() const { return m_->spellcheck_; }
int Configuration::heartbeat() const { return m_->heartbeat_; }
int Configuration::watchdog() const { return m_->watchdog_; }
bool Configuration::TX_messages() const { return m_->TX_messages_; }
bool Configuration::split_mode() const { return m_->split_mode(); }
QString Configuration::opCall() const { return m_->opCall_; }
QString Configuration::ptt_command() const {
return m_->ptt_command_.trimmed();
}
QString Configuration::aprs_server_name() const {
return m_->aprs_server_name_;
}
auto Configuration::aprs_server_port() const -> port_type {
return m_->aprs_server_port_;
}
QString Configuration::udp_server_name() const { return m_->udp_server_name_; }
auto Configuration::udp_server_port() const -> port_type {
return m_->udp_server_port_;
}
QString Configuration::tcp_server_name() const { return m_->tcp_server_name_; }
auto Configuration::tcp_server_port() const -> port_type {
return m_->tcp_server_port_;
}
bool Configuration::accept_udp_requests() const {
return m_->accept_udp_requests_;
}
bool Configuration::accept_tcp_requests() const {
return m_->accept_tcp_requests_;
}
// WSJT-X Protocol accessors
bool Configuration::wsjtx_protocol_enabled() const {
return m_->wsjtx_protocol_enabled_;
}
QString Configuration::wsjtx_server_name() const {
return m_->wsjtx_server_name_;
}
auto Configuration::wsjtx_server_port() const -> port_type {
return m_->wsjtx_server_port_;
}
int Configuration::wsjtx_TTL() const { return m_->wsjtx_TTL_; }
bool Configuration::wsjtx_accept_requests() const {
return m_->wsjtx_accept_requests_;
}
QStringList Configuration::wsjtx_interface_names() const {
return m_->wsjtx_interface_names_;
}
QString Configuration::n3fjp_server_name() const {
return m_->n3fjp_server_name_;
}
auto Configuration::n3fjp_server_port() const -> port_type {
return m_->n3fjp_server_port_;
}
bool Configuration::broadcast_to_n3fjp() const {
return m_->broadcast_to_n3fjp_;
}
QString Configuration::n1mm_server_name() const {
return m_->n1mm_server_name_;
}
auto Configuration::n1mm_server_port() const -> port_type {
return m_->n1mm_server_port_;
}
bool Configuration::broadcast_to_n1mm() const { return m_->broadcast_to_n1mm_; }
bool Configuration::udpEnabled() const { return m_->udpEnabled_; }
bool Configuration::tcpEnabled() const { return m_->tcpEnabled_; }
int Configuration::tcp_max_connections() const {
return m_->tcpMaxConnections_;
}
Bands *Configuration::bands() { return &m_->bands_; }
Bands const *Configuration::bands() const { return &m_->bands_; }
StationList *Configuration::stations() { return &m_->stations_; }
StationList const *Configuration::stations() const { return &m_->stations_; }
bool Configuration::auto_switch_bands() const { return m_->auto_switch_bands_; }
IARURegions::Region Configuration::region() const { return m_->region_; }
FrequencyList_v3 *Configuration::frequencies() { return &m_->frequencies_; }
FrequencyList_v3 const *Configuration::frequencies() const {
return &m_->frequencies_;
}
QStringListModel *Configuration::macros() { return &m_->macros_; }
QStringListModel const *Configuration::macros() const { return &m_->macros_; }
QDir Configuration::save_directory() const { return m_->save_directory_; }
QString Configuration::rig_name() const { return m_->rig_params_.rig_name; }
bool Configuration::pwrBandTxMemory() const { return m_->pwrBandTxMemory_; }
bool Configuration::pwrBandTuneMemory() const { return m_->pwrBandTuneMemory_; }
void Configuration::set_calibration(CalibrationParams params) {
m_->calibration_ = params;
}
void Configuration::enable_calibration(bool on) {
auto target_frequency =
m_->remove_calibration(m_->cached_rig_state_.frequency());
m_->frequency_calibration_disabled_ = !on;
transceiver_frequency(target_frequency);
}
bool Configuration::is_transceiver_online() const { return m_->rig_active_; }
bool Configuration::is_dummy_rig() const { return m_->rig_is_dummy_; }
bool Configuration::transceiver_online() {
qCDebug(configuration_js8)
<< "Configuration::transceiver_online: " << m_->cached_rig_state_;
return m_->have_rig();
}
int Configuration::transceiver_resolution() const {
return m_->rig_resolution_;
}
void Configuration::transceiver_offline() {
qCDebug(configuration_js8)
<< "Configuration::transceiver_offline:" << m_->cached_rig_state_;
m_->close_rig();
}
void Configuration::transceiver_frequency(Frequency f) {
qCDebug(configuration_js8) << "Configuration::transceiver_frequency:" << f
<< m_->cached_rig_state_;
m_->transceiver_frequency(f);
}
void Configuration::transceiver_tx_frequency(Frequency f) {
qCDebug(configuration_js8)
<< "Configuration::transceiver_tx_frequency:" << f
<< m_->cached_rig_state_;
m_->transceiver_tx_frequency(f);
}
void Configuration::transceiver_mode(MODE mode) {
qCDebug(configuration_js8)
<< "Configuration::transceiver_mode:" << mode << m_->cached_rig_state_;
m_->transceiver_mode(mode);
}
void Configuration::transceiver_ptt(bool on) {
qCDebug(configuration_js8)
<< "Configuration::transceiver_ptt:" << on << m_->cached_rig_state_;
m_->transceiver_ptt(on);
if (auto cmd = ptt_command(); !cmd.isEmpty()) {
if (!cmd.contains("%1"))
cmd.append(" %1");
if (auto arguments =
QProcess::splitCommand(cmd.arg(on ? "\"on\"" : "\"off\""));
!arguments.isEmpty()) {
QString program = arguments.takeFirst();
QProcess::startDetached(program, arguments);
}
}
}
void Configuration::sync_transceiver(bool force_signal,
bool enforce_mode_and_split) {
qCDebug(configuration_js8)
<< "Configuration::sync_transceiver: force signal:" << force_signal
<< "enforce_mode_and_split:" << enforce_mode_and_split
<< m_->cached_rig_state_;
m_->sync_transceiver(force_signal);
if (!enforce_mode_and_split) {
m_->transceiver_tx_frequency(0);
}
}
void Configuration::invalidate_audio_input_device(QString /* error */) {
m_->audio_input_device_ = QAudioDevice{};
}
void Configuration::invalidate_audio_output_device(QString /* error */) {
m_->audio_output_device_ = QAudioDevice{};
}
void Configuration::invalidate_notification_audio_output_device(
QString /* error */) {
m_->notification_audio_output_device_ = QAudioDevice{};
}
bool Configuration::valid_n3fjp_info() const {
// do very rudimentary checking on the n3fjp server name and port number.
//
auto server_name = m_->n3fjp_server_name_;
auto port_number = m_->n3fjp_server_port_;
return (!(server_name.trimmed().isEmpty() || port_number == 0));
}
bool Configuration::valid_n1mm_info() const {
// do very rudimentary checking on the n1mm server name and port number.
//
auto server_name = m_->n1mm_server_name_;
auto port_number = m_->n1mm_server_port_;
return (!(server_name.trimmed().isEmpty() || port_number == 0));
}
QString Configuration::my_grid() const {
auto grid = m_->my_grid_;
if (m_->use_dynamic_info_ && m_->dynamic_grid_.size() >= 4) {
grid = m_->dynamic_grid_;
}
return grid.trimmed();
}
QSet<QString> Configuration::my_groups() const {
return QSet<QString>(m_->my_groups_.begin(), m_->my_groups_.end());
}
void Configuration::addGroup(QString const &group) {
if (!Varicode::isGroupAllowed(group)) {
JS8MessageBox::critical_message(
m_->window(),
QString("%1 is a group that cannot be joined").arg(group));
return;
}
if (!Varicode::isCompoundCallsign(group)) {
JS8MessageBox::critical_message(
m_->window(), QString("%1 is not a valid group").arg(group));
return;
}
QSet<QString> groups = my_groups();
groups.insert(group.trimmed());
m_->my_groups_ = groups.values();
m_->write_settings();
}
void Configuration::removeGroup(QString const &group) {
QSet<QString> groups = my_groups();
groups.remove(group.trimmed());
m_->my_groups_ = groups.values();
m_->write_settings();
}
void Configuration::setMyGroups(QStringList const &groups) {
m_->my_groups_ = groups;
m_->write_settings();
}
QSet<QString> Configuration::auto_whitelist() const {
return QSet<QString>(m_->auto_whitelist_.begin(),
m_->auto_whitelist_.end());
}
QSet<QString> Configuration::auto_blacklist() const {
return QSet<QString>(m_->auto_blacklist_.begin(),
m_->auto_blacklist_.end());
}
// automatic HB ACK suppression, adds callsigns to the blacklist
// that send out HB's at intervals less than 55 minutes
void Configuration::addToHbBlacklist(const QString &callsign) {
if (!m_->hb_blacklist_.contains(callsign)) {
m_->hb_blacklist_.append(callsign);
m_->write_settings();
}
}
void Configuration::add_to_rx_callsign_blocklist(const QString &callsign) {
if (!m_->rx_callsign_blocklist_.contains(callsign)) {
m_->rx_callsign_blocklist_.append(callsign);
m_->write_settings();
}
}
QSet<QString> Configuration::hb_blacklist() const {
return QSet<QString>(m_->hb_blacklist_.begin(), m_->hb_blacklist_.end());
}
QSet<QString> Configuration::spot_blacklist() const {
return QSet<QString>(m_->spot_blacklist_.begin(),
m_->spot_blacklist_.end());
}
QSet<QString> Configuration::rx_callsign_blocklist() const {
return QSet<QString>(m_->rx_callsign_blocklist_.begin(),
m_->rx_callsign_blocklist_.end());
}
QSet<QString> Configuration::primary_highlight_words() const {
return QSet<QString>(m_->primary_highlight_words_.begin(),
m_->primary_highlight_words_.end());
}
QSet<QString> Configuration::secondary_highlight_words() const {
return QSet<QString>(m_->secondary_highlight_words_.begin(),
m_->secondary_highlight_words_.end());
}
QString Configuration::eot() const { return m_->eot_; }
QString Configuration::mfi() const { return m_->mfi_; }
QString Configuration::my_info() const {
auto info = m_->my_info_;
if (m_->use_dynamic_info_ && !m_->dynamic_info_.isEmpty()) {
info = m_->dynamic_info_;
}
return info.trimmed();
}
QString Configuration::my_status() const {
auto status = m_->my_status_;
if (m_->use_dynamic_info_ && !m_->dynamic_status_.isEmpty()) {
status = m_->dynamic_status_;
}
return status.trimmed();
}
QString Configuration::hb_message() const { return m_->hb_.trimmed(); }
QString Configuration::cq_message() const {
return m_->cq_.trimmed().replace("CQCQCQ", "CQ CQ CQ"); // deprecate legacy
}
QString Configuration::reply_message() const { return m_->reply_.trimmed(); }
int Configuration::callsign_aging() const { return m_->callsign_aging_; }
int Configuration::activity_aging() const { return m_->activity_aging_; }
void Configuration::set_dynamic_location(QString const &grid_descriptor) {
m_->dynamic_grid_ = grid_descriptor.trimmed();
}
void Configuration::set_dynamic_station_info(QString const &info) {
m_->dynamic_info_ = info.trimmed();
}
void Configuration::set_dynamic_station_status(QString const &status) {
m_->dynamic_status_ = status.trimmed();
}
template <typename T> void setUppercase(T *t) {
auto f = t->font();
f.setCapitalization(QFont::AllUppercase);
t->setFont(f);
}
QWidget *centeredCheckBox(QWidget *parent, QCheckBox **ppCheckbox) {
auto w = new QWidget(parent);
auto cb = new QCheckBox(parent);
auto l = new QHBoxLayout(w);
l->setContentsMargins(1, 1, 1, 1);
l->setAlignment(Qt::AlignCenter);
l->addWidget(cb);
w->setLayout(l);
if (ppCheckbox)
*ppCheckbox = cb;
return w;
}
Configuration::impl::impl(Configuration *self, QDir const &temp_directory,
QSettings *settings, QWidget *parent)
: QDialog{parent}, self_{self}, transceiver_thread_{nullptr},
ui_{new Ui::configuration_dialog}, settings_{settings},
temp_dir_{temp_directory},
writeable_data_dir_{QStandardPaths::writableLocation(
QStandardPaths::AppLocalDataLocation)},
restart_sound_input_device_{false}, restart_sound_output_device_{false},
restart_notification_sound_output_device_{false}, frequencies_{&bands_},
next_frequencies_{&bands_}, stations_{&bands_}, next_stations_{&bands_},
frequency_dialog_{new FrequencyDialog{&regions_, &modes_, this}},
station_dialog_{new StationDialog{&next_stations_, &bands_, this}},
last_port_type_{TransceiverFactory::Capabilities::none},
rig_is_dummy_{false}, rig_active_{false}, have_rig_{false},
rig_changed_{false}, rig_resolution_{0},
frequency_calibration_disabled_{false}, transceiver_command_number_{0},
spot_to_aprs_relay_{false} {
ui_->setupUi(this);
// ui_->groupBox_6->setVisible(false); //### Temporary ??? ###
{
// Find a suitable data file location
if (!writeable_data_dir_.mkpath(".")) {
JS8MessageBox::critical_message(
this, tr("Failed to create data directory"),
tr("path: \"%1\"").arg(writeable_data_dir_.absolutePath()));
throw std::runtime_error{"Failed to create data directory"};
}
// Make sure the default save directory exists
QString save_dir{"save"};
default_save_directory_ = writeable_data_dir_;
if (!default_save_directory_.mkpath(save_dir) ||
!default_save_directory_.cd(save_dir)) {
JS8MessageBox::critical_message(
this, tr("Failed to create save directory"),
tr("path: \"%1\%")
.arg(default_save_directory_.absoluteFilePath(save_dir)));
throw std::runtime_error{"Failed to create save directory"};
}
// we now have a deafult save path that exists
// make sure samples directory exists
QString samples_dir{"samples"};
if (!default_save_directory_.mkpath(samples_dir)) {
JS8MessageBox::critical_message(
this, tr("Failed to create samples directory"),
tr("path: \"%1\"")
.arg(
default_save_directory_.absoluteFilePath(samples_dir)));
throw std::runtime_error{"Failed to create samples directory"};
}
QString messages_dir{"messages"};
if (!default_save_directory_.mkpath(messages_dir)) {
JS8MessageBox::critical_message(
this, tr("Failed to create messages directory"),
tr("path: \"%1\"")
.arg(default_save_directory_.absoluteFilePath(
messages_dir)));
throw std::runtime_error{"Failed to create messages directory"};
}
// copy in any new sample files to the sample directory
QDir dest_dir{default_save_directory_};
dest_dir.cd(samples_dir);
QDir source_dir{":/" + samples_dir};
source_dir.cd(save_dir);
source_dir.cd(samples_dir);
auto list = source_dir.entryInfoList(QStringList{{"*.wav"}},
QDir::Files | QDir::Readable);
Q_FOREACH (auto const &item, list) {
if (!dest_dir.exists(item.fileName())) {
QFile file{item.absoluteFilePath()};
file.copy(dest_dir.absoluteFilePath(item.fileName()));
}
}
}
// this must be done after the default paths above are set
read_settings();
// set up dynamic loading of audio devices
connect(
ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup,
[this]() {
QGuiApplication::setOverrideCursor(QCursor{Qt::WaitCursor});
const bool show_monitors =
settings_->value("AllowMonitorInputs", true).toBool();
load_audio_devices(QAudioDevice::Input,
ui_->sound_input_combo_box,
&next_audio_input_device_,
show_monitors);
update_audio_channels(ui_->sound_input_combo_box,
ui_->sound_input_channel_combo_box,
ui_->sound_input_combo_box->currentIndex(),
false);
ui_->sound_input_channel_combo_box->setCurrentIndex(next_audio_input_channel_);
QGuiApplication::restoreOverrideCursor();
});
connect(ui_->sound_output_combo_box, &LazyFillComboBox::about_to_show_popup,
[this]() {
QGuiApplication::setOverrideCursor(QCursor{Qt::WaitCursor});
load_audio_devices(QAudioDevice::Output,
ui_->sound_output_combo_box,
&next_audio_output_device_);
update_audio_channels(ui_->sound_output_combo_box,
ui_->sound_output_channel_combo_box,
ui_->sound_output_combo_box->currentIndex(),
true);
ui_->sound_output_channel_combo_box->setCurrentIndex(next_audio_output_channel_);
QGuiApplication::restoreOverrideCursor();
});
connect(ui_->notification_sound_output_combo_box,
&LazyFillComboBox::about_to_show_popup, [this]() {
QGuiApplication::setOverrideCursor(QCursor{Qt::WaitCursor});
load_audio_devices(QAudioDevice::Output,
ui_->notification_sound_output_combo_box,
&next_notification_audio_output_device_);
QGuiApplication::restoreOverrideCursor();
});
connect(ui_->run_diagnostics_button, &QPushButton::clicked, [this]() {
run_diagnostics();
});
connect(ui_->copy_diagnostics_button, &QPushButton::clicked, [this]() {
const auto text = ui_->diagnostics_text->toPlainText();
QGuiApplication::clipboard()->setText(text);
});
QFont f = QFontDatabase::systemFont(QFontDatabase::FixedFont);
f.setPointSizeF(f.pointSizeF() * 0.95);
ui_->diagnostics_text->setFont(f);
//
// validation
//
ui_->callsign_line_edit->setValidator(new CallsignValidator{this});
ui_->grid_line_edit->setValidator(new Maidenhead::ExtendedValidator{this});
// theme-aware indication for invalid callsign and grid fields
attachRequiredIndicator(ui_->callsign_line_edit, "Callsign Required");
attachRequiredIndicator(ui_->grid_line_edit, "Grid Required");
ui_->add_macro_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->info_message_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->reply_message_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->cq_message_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->hb_message_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->status_message_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
ui_->groups_line_edit->setValidator(
new QRegularExpressionValidator{message_alphabet, this});
setUppercase(ui_->callsign_line_edit);
setUppercase(ui_->grid_line_edit);
setUppercase(ui_->add_macro_line_edit);
setUppercase(ui_->info_message_line_edit);
setUppercase(ui_->status_message_line_edit);
setUppercase(ui_->reply_message_line_edit);
setUppercase(ui_->cq_message_line_edit);
setUppercase(ui_->hb_message_line_edit);
setUppercase(ui_->groups_line_edit);
setUppercase(ui_->auto_whitelist_line_edit);
ui_->udp_server_port_spin_box->setMinimum(0);
ui_->udp_server_port_spin_box->setMaximum(
std::numeric_limits<port_type>::max());
ui_->n3fjp_server_port_spin_box->setMinimum(0);
ui_->n3fjp_server_port_spin_box->setMaximum(
std::numeric_limits<port_type>::max());
ui_->n1mm_server_port_spin_box->setMinimum(0);
ui_->n1mm_server_port_spin_box->setMaximum(
std::numeric_limits<port_type>::max());
//
// assign ids to radio buttons
//
ui_->CAT_data_bits_button_group->setId(
ui_->CAT_default_bit_radio_button,
TransceiverFactory::default_data_bits);
ui_->CAT_data_bits_button_group->setId(ui_->CAT_7_bit_radio_button,
TransceiverFactory::seven_data_bits);
ui_->CAT_data_bits_button_group->setId(ui_->CAT_8_bit_radio_button,
TransceiverFactory::eight_data_bits);
ui_->CAT_stop_bits_button_group->setId(
ui_->CAT_default_stop_bit_radio_button,
TransceiverFactory::default_stop_bits);
ui_->CAT_stop_bits_button_group->setId(ui_->CAT_one_stop_bit_radio_button,
TransceiverFactory::one_stop_bit);
ui_->CAT_stop_bits_button_group->setId(ui_->CAT_two_stop_bit_radio_button,
TransceiverFactory::two_stop_bits);
ui_->CAT_handshake_button_group->setId(
ui_->CAT_handshake_default_radio_button,
TransceiverFactory::handshake_default);
ui_->CAT_handshake_button_group->setId(ui_->CAT_handshake_none_radio_button,
TransceiverFactory::handshake_none);
ui_->CAT_handshake_button_group->setId(
ui_->CAT_handshake_xon_radio_button,
TransceiverFactory::handshake_XonXoff);
ui_->CAT_handshake_button_group->setId(
ui_->CAT_handshake_hardware_radio_button,
TransceiverFactory::handshake_hardware);
ui_->PTT_method_button_group->setId(ui_->PTT_VOX_radio_button,
TransceiverFactory::PTT_method_VOX);
ui_->PTT_method_button_group->setId(ui_->PTT_CAT_radio_button,
TransceiverFactory::PTT_method_CAT);
ui_->PTT_method_button_group->setId(ui_->PTT_DTR_radio_button,
TransceiverFactory::PTT_method_DTR);
ui_->PTT_method_button_group->setId(ui_->PTT_RTS_radio_button,
TransceiverFactory::PTT_method_RTS);
ui_->TX_audio_source_button_group->setId(
ui_->TX_source_mic_radio_button,
TransceiverFactory::TX_audio_source_front);
ui_->TX_audio_source_button_group->setId(
ui_->TX_source_data_radio_button,
TransceiverFactory::TX_audio_source_rear);
ui_->TX_mode_button_group->setId(ui_->mode_none_radio_button,
data_mode_none);
ui_->TX_mode_button_group->setId(ui_->mode_USB_radio_button, data_mode_USB);
ui_->TX_mode_button_group->setId(ui_->mode_data_radio_button,
data_mode_data);
ui_->split_mode_button_group->setId(ui_->split_none_radio_button,
TransceiverFactory::split_mode_none);
ui_->split_mode_button_group->setId(ui_->split_rig_radio_button,
TransceiverFactory::split_mode_rig);
ui_->split_mode_button_group->setId(ui_->split_emulate_radio_button,
TransceiverFactory::split_mode_emulate);
//
// setup PTT port combo box drop down content
//
fill_port_combo_box(ui_->PTT_port_combo_box);
ui_->PTT_port_combo_box->addItem("CAT");
//
// setup hooks to keep audio channels aligned with devices
//
connect(
ui_->sound_input_combo_box,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
[this](auto const index) {
update_audio_channels(ui_->sound_input_combo_box,
ui_->sound_input_channel_combo_box, index,
false);
});
connect(
ui_->sound_output_combo_box,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
[this](auto const index) {
update_audio_channels(ui_->sound_output_combo_box,
ui_->sound_output_channel_combo_box, index,
true);
});
//
// setup macros list view
//
ui_->macros_list_view->setModel(&next_macros_);
ui_->macros_list_view->setItemDelegate(new MessageItemDelegate{this});
macro_delete_action_ = new QAction{tr("&Delete"), ui_->macros_list_view};
ui_->macros_list_view->insertAction(nullptr, macro_delete_action_);
connect(macro_delete_action_, &QAction::triggered, this,
&Configuration::impl::delete_macro);
// setup IARU region combo box model
ui_->region_combo_box->setModel(&regions_);
//
// setup working frequencies table model & view
//
frequencies_.sort(FrequencyList_v3::frequency_column);
ui_->frequencies_table_view->setModel(&next_frequencies_);
ui_->frequencies_table_view->sortByColumn(
FrequencyList_v3::frequency_column, Qt::AscendingOrder);
ui_->frequencies_table_view->setColumnHidden(
FrequencyList_v3::frequency_mhz_column, true);
// delegates
auto frequencies_item_delegate = new QStyledItemDelegate{this};
frequencies_item_delegate->setItemEditorFactory(item_editor_factory());
ui_->frequencies_table_view->setItemDelegate(frequencies_item_delegate);
ui_->frequencies_table_view->setItemDelegateForColumn(
FrequencyList_v3::region_column,
new ForeignKeyDelegate{&regions_, 0, this});
ui_->frequencies_table_view->setItemDelegateForColumn(
FrequencyList_v3::mode_column,
new ForeignKeyDelegate{&modes_, 0, this});
// actions
frequency_delete_action_ =
new QAction{tr("&Delete"), ui_->frequencies_table_view};
frequency_delete_action_->setShortcut(QKeySequence(Qt::Key_Delete));
frequency_delete_action_->setShortcutContext(Qt::WidgetWithChildrenShortcut);
ui_->frequencies_table_view->insertAction(nullptr,
frequency_delete_action_);
connect(frequency_delete_action_, &QAction::triggered, this,
&Configuration::impl::delete_frequencies);
frequency_insert_action_ =
new QAction{tr("&Insert ..."), ui_->frequencies_table_view};
ui_->frequencies_table_view->insertAction(nullptr,
frequency_insert_action_);
connect(frequency_insert_action_, &QAction::triggered, this,
&Configuration::impl::insert_frequency);
load_frequencies_action_ =
new QAction{tr("&Load ..."), ui_->frequencies_table_view};
ui_->frequencies_table_view->insertAction(nullptr,
load_frequencies_action_);
connect(load_frequencies_action_, &QAction::triggered, this,
&Configuration::impl::load_frequencies);
save_frequencies_action_ =
new QAction{tr("&Save as ..."), ui_->frequencies_table_view};
ui_->frequencies_table_view->insertAction(nullptr,
save_frequencies_action_);
connect(save_frequencies_action_, &QAction::triggered, this,
&Configuration::impl::save_frequencies);
merge_frequencies_action_ =
new QAction{tr("&Merge ..."), ui_->frequencies_table_view};
ui_->frequencies_table_view->insertAction(nullptr,
merge_frequencies_action_);
connect(merge_frequencies_action_, &QAction::triggered, this,
&Configuration::impl::merge_frequencies);
reset_frequencies_action_ =
new QAction{tr("&Reset"), ui_->frequencies_table_view};
ui_->frequencies_table_view->insertAction(nullptr,
reset_frequencies_action_);
connect(reset_frequencies_action_, &QAction::triggered, this,
&Configuration::impl::reset_frequencies);
//
// setup stations table model & view
//
stations_.sort(StationList::switch_at_column);
ui_->stations_table_view->setModel(&next_stations_);
ui_->stations_table_view->sortByColumn(StationList::switch_at_column,
Qt::AscendingOrder);
connect(ui_->auto_switch_bands_check_box, &QCheckBox::clicked,
ui_->stations_table_view, &QTableView::setEnabled);
// Immediately emit the auto switch band change signal to kick the scheduler
// without having to potentially save twice
connect(ui_->auto_switch_bands_check_box, &QCheckBox::clicked, this,
[this](bool auto_switch_bands) {
Q_EMIT self_->auto_switch_bands_changed(auto_switch_bands);
});
// delegates
auto stations_item_delegate = new QStyledItemDelegate{this};
stations_item_delegate->setItemEditorFactory(item_editor_factory());
ui_->stations_table_view->setItemDelegate(stations_item_delegate);
// ui_->stations_table_view->setItemDelegateForColumn
// (StationList::band_column, new ForeignKeyDelegate {&bands_,
// &next_stations_, 0, StationList::band_column, this});
ui_->stations_table_view->resizeColumnToContents(StationList::band_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::frequency_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_at_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_until_column);
ui_->stations_table_view->horizontalHeader()->setSectionResizeMode(
QHeaderView::ResizeToContents);
// actions
station_hop_action_ =
new QAction{tr("&Switch to Frequency Now"), ui_->stations_table_view};
ui_->stations_table_view->insertAction(nullptr, station_hop_action_);
connect(station_hop_action_, &QAction::triggered, this,
&Configuration::impl::hop_to_station);
station_delete_action_ =
new QAction{tr("&Delete"), ui_->stations_table_view};
station_delete_action_->setShortcut(QKeySequence(Qt::Key_Delete));
station_delete_action_->setShortcutContext(Qt::WidgetWithChildrenShortcut);
ui_->stations_table_view->insertAction(nullptr, station_delete_action_);
connect(station_delete_action_, &QAction::triggered, this,
&Configuration::impl::delete_stations);
station_insert_action_ =
new QAction{tr("&Insert ..."), ui_->stations_table_view};
ui_->stations_table_view->insertAction(nullptr, station_insert_action_);
connect(station_insert_action_, &QAction::triggered, this,
&Configuration::impl::insert_station);
// Connect to selection changes and update action states
auto selection_model = ui_->stations_table_view->selectionModel();
connect(selection_model, &QItemSelectionModel::selectionChanged, this,
[this]() {
auto selected_rows =
ui_->stations_table_view->selectionModel()->selectedRows();
bool has_selection = !selected_rows.isEmpty();
bool has_single_selection = (selected_rows.size() == 1);
station_delete_action_->setVisible(has_selection);
station_hop_action_->setVisible(has_single_selection);
});
// Initial state
station_hop_action_->setVisible(false);
station_delete_action_->setVisible(false);
enumerate_rigs();
initialize_models();
audio_input_device_ = next_audio_input_device_;
audio_input_channel_ = next_audio_input_channel_;
audio_output_device_ = next_audio_output_device_;
audio_output_channel_ = next_audio_output_channel_;
notification_audio_output_device_ = next_notification_audio_output_device_;
transceiver_thread_ = new QThread{this};
transceiver_thread_->start();
}
Configuration::impl::~impl() {
transceiver_thread_->quit();
transceiver_thread_->wait();
write_settings();
}
void Configuration::impl::initialize_models() {
next_audio_input_device_ = audio_input_device_;
next_audio_input_channel_ = audio_input_channel_;
next_audio_output_device_ = audio_output_device_;
next_audio_output_channel_ = audio_output_channel_;
notification_audio_output_device_ = next_notification_audio_output_device_;
restart_sound_input_device_ = false;
restart_sound_output_device_ = false;
restart_notification_sound_output_device_ = false;
{
SettingsGroup g{settings_, "Configuration"};
find_audio_devices();
}
ui_->auto_switch_bands_check_box->setChecked(auto_switch_bands_);
ui_->callsign_line_edit->setText(my_callsign_);
ui_->grid_line_edit->setText(my_grid_.toUpper());
ui_->callsign_aging_spin_box->setValue(callsign_aging_);
ui_->activity_aging_spin_box->setValue(activity_aging_);
ui_->groups_line_edit->setText(my_groups_.join(", "));
ui_->auto_whitelist_line_edit->setText(auto_whitelist_.join(", "));
ui_->auto_blacklist_line_edit->setText(auto_blacklist_.join(", "));
ui_->hb_blacklist_line_edit->setText(hb_blacklist_.join(", "));
ui_->hb_rate_limit_check_box->setChecked(hb_rate_limit_);
ui_->spot_blacklist_line_edit->setText(spot_blacklist_.join(", "));
ui_->rx_blocklist_line_edit->setText(rx_callsign_blocklist_.join(", "));
ui_->primaryHighlightLineEdit->setText(primary_highlight_words_.join(", "));
ui_->secondaryHighlightLineEdit->setText(
secondary_highlight_words_.join(", "));
ui_->eot_line_edit->setText(eot_);
ui_->mfi_line_edit->setText(mfi_);
ui_->info_message_line_edit->setText(my_info_.toUpper());
ui_->status_message_line_edit->setText(my_status_.toUpper());
ui_->cq_message_line_edit->setText(
cq_.toUpper().replace("CQCQCQ", "CQ CQ CQ"));
ui_->hb_message_line_edit->setText(hb_.toUpper());
ui_->reply_message_line_edit->setText(reply_.toUpper());
ui_->use_dynamic_grid->setChecked(use_dynamic_info_);
ui_->tableForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
ui_->tableBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
ui_->tableSelectionBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->primaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_primary_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->secondaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_secondary_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->cqMessagesLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_cq_.name())
.arg(next_color_table_foreground_.name()));
ui_->labMyCall->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_mycall_.name())
.arg(next_color_table_foreground_.name()));
ui_->rxLabel->setStyleSheet(QString("background: %1; color: %2")
.arg(color_rx_background_.name())
.arg(color_rx_foreground_.name()));
ui_->rxForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(color_rx_background_.name())
.arg(color_rx_foreground_.name()));
ui_->txForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(color_rx_background_.name())
.arg(color_tx_foreground_.name()));
ui_->composeLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(color_compose_background_.name())
.arg(color_compose_foreground_.name()));
ui_->pills_enabled_check_box->setChecked(pills_enabled_);
updatePillPreview(ui_->pillRecipientLabel, next_color_pill_recipient_bg_,
next_color_pill_recipient_fg_);
updatePillPreview(ui_->pillCommandLabel, next_color_pill_command_bg_,
next_color_pill_command_fg_);
updatePillPreview(ui_->pillGroupLabel, next_color_pill_group_bg_,
next_color_pill_group_fg_);
updatePillPreview(ui_->pillSenderLabel, next_color_pill_sender_bg_,
next_color_pill_sender_fg_);
ui_->sbTxDelay->setValue(txDelay_);
ui_->PTT_method_button_group->button(rig_params_.ptt_type)
->setChecked(true);
ui_->save_path_display_label->setText(save_directory_.absolutePath());
ui_->write_logs_check_box->setChecked(write_logs_);
ui_->reset_activity_check_box->setChecked(reset_activity_);
ui_->checkForUpdates_checkBox->setChecked(check_for_updates_);
ui_->tx_qsy_check_box->setChecked(tx_qsy_allowed_);
ui_->psk_reporter_check_box->setChecked(spot_to_reporting_networks_);
ui_->enable_aprs_spotting_check_box->setChecked(spot_to_aprs_);
ui_->enable_aprs_relay_check_box->setChecked(spot_to_aprs_relay_);
ui_->transmit_directed_check_box->setChecked(transmit_directed_);
ui_->autoreply_on_check_box->setChecked(autoreply_on_at_startup_);
ui_->autoreply_confirmation_check_box->setChecked(autoreply_confirmation_);
ui_->heartbeat_anywhere_check_box->setChecked(heartbeat_anywhere_);
ui_->heartbeat_qso_pause_check_box->setChecked(heartbeat_qso_pause_);
ui_->heartbeat_ack_snr_check_box->setChecked(heartbeat_ack_snr_);
ui_->relay_disabled_check_box->setChecked(relay_disabled_);
ui_->monitor_off_check_box->setChecked(monitor_off_at_startup_);
ui_->transmit_off_check_box->setChecked(transmit_off_at_startup_);
ui_->monitor_last_used_check_box->setChecked(monitor_last_used_);
ui_->stations_table_view->setEnabled(
ui_->auto_switch_bands_check_box->isChecked());
ui_->miles_check_box->setChecked(miles_);
ui_->hold_ptt_check_box->setChecked(hold_ptt_);
ui_->avoid_forced_identify_check_box->setChecked(avoid_forced_identify_);
ui_->avoid_allcall_check_box->setChecked(avoid_allcall_);
ui_->spellcheck_check_box->setChecked(spellcheck_);
ui_->heartbeat_spin_box->setValue(heartbeat_);
ui_->tx_watchdog_spin_box->setValue(watchdog_);
ui_->rig_combo_box->setCurrentText(rig_params_.rig_name);
ui_->TX_mode_button_group->button(data_mode_)->setChecked(true);
ui_->split_mode_button_group->button(rig_params_.split_mode)
->setChecked(true);
ui_->CAT_serial_baud_combo_box->setCurrentText(
QString::number(rig_params_.baud));
ui_->CAT_data_bits_button_group->button(rig_params_.data_bits)
->setChecked(true);
ui_->CAT_stop_bits_button_group->button(rig_params_.stop_bits)
->setChecked(true);
ui_->CAT_handshake_button_group->button(rig_params_.handshake)
->setChecked(true);
ui_->checkBoxPwrBandTxMemory->setChecked(pwrBandTxMemory_);
ui_->checkBoxPwrBandTuneMemory->setChecked(pwrBandTuneMemory_);
if (rig_params_.force_dtr) {
ui_->force_DTR_combo_box->setCurrentIndex(rig_params_.dtr_high ? 1 : 2);
} else {
ui_->force_DTR_combo_box->setCurrentIndex(0);
}
if (rig_params_.force_rts) {
ui_->force_RTS_combo_box->setCurrentIndex(rig_params_.rts_high ? 1 : 2);
} else {
ui_->force_RTS_combo_box->setCurrentIndex(0);
}
ui_->TX_audio_source_button_group->button(rig_params_.audio_source)
->setChecked(true);
ui_->CAT_poll_interval_spin_box->setValue(rig_params_.poll_interval);
ui_->opCallEntry->setText(opCall_);
ui_->ptt_command_line_edit->setText(ptt_command_);
ui_->aprs_server_line_edit->setText(aprs_server_name_);
ui_->aprs_server_port_spin_box->setValue(aprs_server_port_);
ui_->udp_server_line_edit->setText(udp_server_name_);
ui_->udp_server_port_spin_box->setValue(udp_server_port_);
ui_->tcp_server_line_edit->setText(tcp_server_name_);
ui_->tcp_server_port_spin_box->setValue(tcp_server_port_);
ui_->accept_udp_requests_check_box->setChecked(accept_udp_requests_);
ui_->accept_tcp_requests_check_box->setChecked(accept_tcp_requests_);
ui_->n3fjp_server_name_line_edit->setText(n3fjp_server_name_);
ui_->n3fjp_server_port_spin_box->setValue(n3fjp_server_port_);
ui_->enable_n3fjp_broadcast_check_box->setChecked(broadcast_to_n3fjp_);
ui_->n1mm_server_name_line_edit->setText(n1mm_server_name_);
ui_->n1mm_server_port_spin_box->setValue(n1mm_server_port_);
ui_->enable_n1mm_broadcast_check_box->setChecked(broadcast_to_n1mm_);
ui_->tcpEnable->setChecked(tcpEnabled_);
ui_->udpEnable->setChecked(udpEnabled_);
ui_->tcp_max_connections_spin_box->setValue(tcpMaxConnections_);
// WSJT-X Protocol settings
ui_->wsjtx_enable_check_box->setChecked(wsjtx_protocol_enabled_);
ui_->wsjtx_server_line_edit->setText(wsjtx_server_name_);
ui_->wsjtx_server_port_spin_box->setValue(wsjtx_server_port_);
ui_->wsjtx_TTL_spin_box->setValue(wsjtx_TTL_);
ui_->wsjtx_accept_requests_check_box->setChecked(wsjtx_accept_requests_);
load_network_interfaces(ui_->wsjtx_interfaces_combo_box,
wsjtx_interface_names_);
ui_->calibration_intercept_spin_box->setValue(calibration_.intercept);
ui_->calibration_slope_ppm_spin_box->setValue(calibration_.slope_ppm);
if (rig_params_.ptt_port.isEmpty()) {
if (ui_->PTT_port_combo_box->count()) {
ui_->PTT_port_combo_box->setCurrentText(
ui_->PTT_port_combo_box->itemText(0));
}
} else {
ui_->PTT_port_combo_box->setCurrentText(rig_params_.ptt_port);
}
ui_->region_combo_box->setCurrentIndex(region_);
next_macros_.setStringList(macros_.stringList());
next_frequencies_.frequency_list(frequencies_.frequency_list());
next_stations_.station_list(stations_.station_list());
//
// setup notifications table view
//
ui_->notifications_check_box->setChecked(enable_notifications_);
on_notifications_check_box_toggled(enable_notifications_);
QList<QPair<QString, QString>> notifyRows = {
{"cq", "CQ Message Received"},
{"hb", "HB Message Received"},
{"ack", "ACK Message Received"},
{"directed", "Directed Message Received"},
{"inbox", "Inbox Message Received"},
{"call_new", "New Callsign Heard"},
{"call_old", "Worked Callsign Heard"},
};
int i = 0;
auto table = ui_->notifications_table_widget;
for (int i = ui_->notifications_table_widget->rowCount() - 1; i >= 0; i--) {
ui_->notifications_table_widget->removeRow(i);
}
auto header = table->horizontalHeader();
header->setStretchLastSection(false);
header->setSectionResizeMode(0, QHeaderView::ResizeToContents);
header->setSectionResizeMode(1, QHeaderView::ResizeToContents);
header->setSectionResizeMode(2, QHeaderView::Stretch);
header->setSectionResizeMode(3, QHeaderView::ResizeToContents);
foreach (auto pair, notifyRows) {
QString key = pair.first;
QString value = pair.second;
bool enabled = notifications_enabled_.value(key, false);
QString path = notifications_paths_.value(key, "");
QCheckBox *enabledCheckbox;
auto enabledWidget = centeredCheckBox(this, &enabledCheckbox);
enabledWidget->setMinimumWidth(100);
if (enabledCheckbox) {
enabledCheckbox->setObjectName("enabledCheckbox");
enabledCheckbox->setChecked(enabled);
}
auto expandingPolicy =
QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
expandingPolicy.setHorizontalStretch(1);
QLabel *pathLabel = new QLabel(this);
pathLabel->setText(path);
pathLabel->setSizePolicy(expandingPolicy);
auto minimumPolicy =
QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
minimumPolicy.setHorizontalStretch(0);
QWidget *buttonWidget = new QWidget(this);
buttonWidget->setSizePolicy(minimumPolicy);
QHBoxLayout *buttonLayout = new QHBoxLayout(buttonWidget);
buttonLayout->setStretch(0, 0);
buttonLayout->setStretch(1, 0);
buttonLayout->setSpacing(3);
buttonLayout->setContentsMargins(3, 1, 3, 1);
buttonWidget->setLayout(buttonLayout);
QPushButton *selectFilePushButton = new QPushButton("Select", this);
selectFilePushButton->setSizePolicy(minimumPolicy);
buttonLayout->addWidget(selectFilePushButton);
connect(selectFilePushButton, &QPushButton::pressed, this,
[this, pathLabel]() {
auto dir = QStandardPaths::standardLocations(
QStandardPaths::MusicLocation);
auto path = QFileDialog::getOpenFileName(
this, "Audio Notification", dir.first(),
"Wave Files (*.wav);;All files (*.*)");
if (!path.isEmpty()) {
pathLabel->setText(path);
}
});
QPushButton *testPushButton = new QPushButton("Test", this);
testPushButton->setSizePolicy(minimumPolicy);
buttonLayout->addWidget(testPushButton);
connect(testPushButton, &QPushButton::pressed, this,
[this, key]() {
emit this->self_->test_notify(key);
});
QPushButton *clearPushButton = new QPushButton("Clear", this);
clearPushButton->setSizePolicy(minimumPolicy);
buttonLayout->addWidget(clearPushButton);
connect(clearPushButton, &QPushButton::pressed, this,
[pathLabel, enabledCheckbox]() {
pathLabel->clear();
enabledCheckbox->setChecked(false);
});
// row config
int col = 0;
table->insertRow(i);
//
auto eventLabelItem = new QTableWidgetItem(value);
eventLabelItem->setData(Qt::UserRole, QVariant(key));
table->setItem(i, col++, eventLabelItem);
table->setCellWidget(i, col++, enabledWidget);
table->setCellWidget(i, col++, pathLabel);
table->setCellWidget(i, col++, buttonWidget);
i++;
}
table->resizeRowsToContents();
table->resizeColumnsToContents();
set_rig_invariants();
}
void Configuration::impl::done(int r) {
// do this here since window is still on screen at this point
SettingsGroup g{settings_, "Configuration"};
settings_->setValue("WindowGeometry", saveGeometry());
QDialog::done(r);
}
void Configuration::impl::read_settings() {
SettingsGroup g{settings_, "Configuration"};
setMinimumSize(800, 400);
restoreGeometry(settings_->value("WindowGeometry").toByteArray());
setMinimumSize(800, 400);
auto_switch_bands_ = settings_->value("AutoSwitchBands", false).toBool();
my_callsign_ = settings_->value("MyCall", QString{}).toString();
my_grid_ = settings_->value("MyGrid", QString{}).toString();
my_groups_ = settings_->value("MyGroups", QStringList{}).toStringList();
auto_whitelist_ =
settings_->value("AutoWhitelist", QStringList{}).toStringList();
auto_blacklist_ =
settings_->value("AutoBlacklist", QStringList{}).toStringList();
hb_blacklist_ =
settings_->value("HBBlacklist", QStringList{}).toStringList();
hb_rate_limit_ = settings_->value("HBRateLimit", false).toBool();
spot_blacklist_ =
settings_->value("SpotBlacklist", QStringList{}).toStringList();
rx_callsign_blocklist_ =
settings_->value("RXCallsignBlocklist", QStringList{}).toStringList();
primary_highlight_words_ =
settings_->value("PrimaryHighlightWords", QStringList{}).toStringList();
secondary_highlight_words_ =
settings_->value("SecondaryHighlightWords", QStringList{})
.toStringList();
callsign_aging_ = settings_->value("CallsignAging", 0).toInt();
activity_aging_ = settings_->value("ActivityAging", 2).toInt();
eot_ = settings_->value("EOTCharacter", QString{"\u2662"}).toString();
mfi_ = settings_->value("MFICharacter", QString{"\u2026\u2026"}).toString();
my_info_ = settings_->value("MyInfo", QString{}).toString();
my_status_ =
settings_
->value("MyStatus", QString{"IDLE <MYIDLE> VERSION <MYVERSION>"})
.toString();
hb_ = settings_->value("HBMessage", QString{"HB <MYGRID4>"}).toString();
cq_ =
settings_->value("CQMessage", QString{"CQ CQ CQ <MYGRID4>"}).toString();
reply_ = settings_->value("Reply", QString{"HW CPY?"}).toString();
next_color_cq_ = color_cq_ =
settings_->value("colorCQ", "#66ff66").toString();
next_color_primary_highlight_ = color_primary_highlight_ =
settings_->value("colorPrimary", "#f1c40f").toString();
next_color_secondary_highlight_ = color_secondary_highlight_ =
settings_->value("colorSecondary", "#ffff66").toString();
next_color_mycall_ = color_mycall_ =
settings_->value("colorMyCall", "#ff6666").toString();
next_color_rx_background_ = color_rx_background_ =
settings_->value("color_rx_background", "#ffeaa7").toString();
next_color_rx_foreground_ = color_rx_foreground_ =
settings_->value("color_rx_foreground", "#000000").toString();
next_color_compose_background_ = color_compose_background_ =
settings_->value("color_compose_background", "#ffffff").toString();
next_color_compose_foreground_ = color_compose_foreground_ =
settings_->value("color_compose_foreground", "#000000").toString();
next_color_tx_foreground_ = color_tx_foreground_ =
settings_->value("color_tx_foreground", "#ff0000").toString();
next_color_DXCC_ = color_DXCC_ =
settings_->value("colorDXCC", "#ff00ff").toString();
next_color_NewCall_ = color_NewCall_ =
settings_->value("colorNewCall", "#ffaaff").toString();
next_color_table_background_ = color_table_background_ =
settings_->value("colorTableBackground", "#ffffff").toString();
next_color_table_highlight_ = color_table_highlight_ =
settings_->value("colorTableHighlight", "#3498db").toString();
next_color_table_foreground_ = color_table_foreground_ =
settings_->value("colorTableForeground", "#000000").toString();
pills_enabled_ = settings_->value("PillsEnabled", true).toBool();
next_color_pill_recipient_bg_ = color_pill_recipient_bg_ =
settings_->value("colorPillRecipientBg", "#2980b9").toString();
next_color_pill_recipient_fg_ = color_pill_recipient_fg_ =
settings_->value("colorPillRecipientFg", "#ffffff").toString();
next_color_pill_command_bg_ = color_pill_command_bg_ =
settings_->value("colorPillCommandBg", "#e67e22").toString();
next_color_pill_command_fg_ = color_pill_command_fg_ =
settings_->value("colorPillCommandFg", "#ffffff").toString();
next_color_pill_group_bg_ = color_pill_group_bg_ =
settings_->value("colorPillGroupBg", "#16a085").toString();
next_color_pill_group_fg_ = color_pill_group_fg_ =
settings_->value("colorPillGroupFg", "#ffffff").toString();
next_color_pill_sender_bg_ = color_pill_sender_bg_ =
settings_->value("colorPillSenderBg", "#7f8c8d").toString();
next_color_pill_sender_fg_ = color_pill_sender_fg_ =
settings_->value("colorPillSenderFg", "#ffffff").toString();
if (next_font_.fromString(
settings_->value("Font", QGuiApplication::font().toString())
.toString()) &&
next_font_ != font_) {
font_ = next_font_;
Q_EMIT self_->gui_text_font_changed(font_);
} else {
next_font_ = font_;
}
ui_->font_push_button->setText(QString("Application Font (%1 %2)")
.arg(next_font_.family())
.arg(next_font_.pointSize()));
if (next_tx_text_font_.fromString(
settings_->value("TXTextFont", QGuiApplication::font().toString())
.toString()) &&
next_tx_text_font_ != tx_text_font_) {
tx_text_font_ = next_tx_text_font_;
Q_EMIT self_->tx_text_font_changed(tx_text_font_);
} else {
next_tx_text_font_ = tx_text_font_;
}
ui_->txFontButton->setText(QString("Font (%1 %2)")
.arg(next_tx_text_font_.family())
.arg(next_tx_text_font_.pointSize()));
if (next_rx_text_font_.fromString(
settings_->value("RXTextFont", QGuiApplication::font().toString())
.toString()) &&
next_rx_text_font_ != rx_text_font_) {
rx_text_font_ = next_rx_text_font_;
Q_EMIT self_->rx_text_font_changed(rx_text_font_);
} else {
next_rx_text_font_ = rx_text_font_;
}
ui_->rxFontButton->setText(QString("Font (%1 %2)")
.arg(next_rx_text_font_.family())
.arg(next_rx_text_font_.pointSize()));
if (next_compose_text_font_.fromString(
settings_
->value("composeTextFont", QGuiApplication::font().toString())
.toString()) &&
next_compose_text_font_ != compose_text_font_) {
compose_text_font_ = next_compose_text_font_;
Q_EMIT self_->compose_text_font_changed(compose_text_font_);
} else {
next_compose_text_font_ = compose_text_font_;
}
ui_->composeFontButton->setText(
QString("Font (%1 %2)")
.arg(next_compose_text_font_.family())
.arg(next_compose_text_font_.pointSize()));
if (next_table_font_.fromString(
settings_->value("tableFont", QGuiApplication::font().toString())
.toString()) &&
next_table_font_ != table_font_) {
table_font_ = next_table_font_;
Q_EMIT self_->table_font_changed(table_font_);
} else {
next_table_font_ = table_font_;
}
ui_->tableFontButton->setText(QString("Font (%1 %2)")
.arg(next_table_font_.family())
.arg(next_table_font_.pointSize()));
txDelay_ = settings_->value("TxDelay", 0.2).toDouble();
save_directory_.setPath(
settings_->value("SaveDir", default_save_directory_.absolutePath())
.toString());
// retrieve audio channel info
audio_input_channel_ = AudioDevice::fromString(
settings_->value("AudioInputChannel", "Mono").toString());
audio_output_channel_ = AudioDevice::fromString(
settings_->value("AudioOutputChannel", "Mono").toString());
transmit_directed_ = settings_->value("TransmitDirected", true).toBool();
autoreply_on_at_startup_ =
settings_->value("AutoreplyOnAtStartup", true).toBool();
autoreply_confirmation_ =
settings_->value("AutoreplyConfirmation", true).toBool();
heartbeat_anywhere_ = settings_->value("BeaconAnywhere", false).toBool();
heartbeat_qso_pause_ = settings_->value("HeartbeatQSOPause", true).toBool();
heartbeat_ack_snr_ = settings_->value("HeartbeatAckSNR", false).toBool();
relay_disabled_ = settings_->value("RelayOFF", false).toBool();
monitor_off_at_startup_ = settings_->value("MonitorOFF", false).toBool();
transmit_off_at_startup_ = settings_->value("TransmitOFF", false).toBool();
monitor_last_used_ = settings_->value("MonitorLastUsed", false).toBool();
spot_to_reporting_networks_ =
settings_->value("PSKReporter", true).toBool();
spot_to_aprs_ = settings_->value("SpotToAPRS", true).toBool();
spot_to_aprs_relay_ = settings_->value("APRSISRelay", false).toBool();
write_logs_ = settings_->value("WriteLogs", true).toBool();
reset_activity_ = settings_->value("ResetActivity", false).toBool();
check_for_updates_ = settings_->value("CheckForUpdates", true).toBool();
tx_qsy_allowed_ = settings_->value("TxQSYAllowed", false).toBool();
use_dynamic_info_ = settings_->value("AutoGrid", false).toBool();
auto loadedMacros =
settings_->value("Macros", QStringList{"TNX 73 GL"}).toStringList();
macros_.setStringList(loadedMacros);
region_ = settings_->value("Region", QVariant::fromValue(IARURegions::ALL))
.value<IARURegions::Region>();
if (settings_->contains(versionedFrequenciesSettingsKey)) {
auto const &v = settings_->value(versionedFrequenciesSettingsKey);
if (v.isValid()) {
frequencies_.frequency_list(
v.value<FrequencyList_v3::FrequencyItems>());
} else {
frequencies_.reset_to_defaults();
}
} else // No new key: try to migrate from old key
if (settings_->contains(v2FrequenciesSettingsKey)) {
const QVariant v = settings_->value(v2FrequenciesSettingsKey);
if (v.isValid()) {
auto oldItems = v.value<FrequencyList_v2::FrequencyItems>();
FrequencyList_v3::FrequencyItems newItems;
newItems.reserve(oldItems.size());
for (const auto& it : oldItems) {
FrequencyList_v3::Item n;
n.frequency_ = it.frequency_;
n.mode_ = it.mode_;
n.region_ = it.region_;
n.description_.clear();
newItems.push_back(std::move(n));
}
frequencies_.frequency_list(std::move(newItems));
// Persist under new key
settings_->setValue(versionedFrequenciesSettingsKey, QVariant::fromValue(frequencies_.frequency_list()));
// Optionally remove the old version
//settings_->remove(v2FrequenciesSettingsKey);
write_settings();
}
} else {
frequencies_.reset_to_defaults();
}
stations_.station_list(
settings_->value("stations").value<StationList::Stations>());
rig_params_.rig_name =
settings_->value("Rig", TransceiverFactory::basic_transceiver_name_)
.toString();
rig_is_dummy_ =
TransceiverFactory::basic_transceiver_name_ == rig_params_.rig_name;
rig_params_.network_port = settings_->value("CATNetworkPort").toString();
rig_params_.usb_port = settings_->value("CATUSBPort").toString();
rig_params_.serial_port = settings_->value("CATSerialPort").toString();
rig_params_.baud = settings_->value("CATSerialRate", 4800).toInt();
rig_params_.data_bits =
settings_
->value("CATDataBits",
QVariant::fromValue(TransceiverFactory::default_data_bits))
.value<TransceiverFactory::DataBits>();
rig_params_.stop_bits =
settings_
->value("CATStopBits",
QVariant::fromValue(TransceiverFactory::default_stop_bits))
.value<TransceiverFactory::StopBits>();
rig_params_.handshake =
settings_
->value("CATHandshake",
QVariant::fromValue(TransceiverFactory::handshake_default))
.value<TransceiverFactory::Handshake>();
rig_params_.force_dtr = settings_->value("CATForceDTR", false).toBool();
rig_params_.dtr_high = settings_->value("DTR", false).toBool();
rig_params_.force_rts = settings_->value("CATForceRTS", false).toBool();
rig_params_.rts_high = settings_->value("RTS", false).toBool();
rig_params_.ptt_type =
settings_
->value("PTTMethod",
QVariant::fromValue(TransceiverFactory::PTT_method_VOX))
.value<TransceiverFactory::PTTMethod>();
rig_params_.audio_source =
settings_
->value(
"TXAudioSource",
QVariant::fromValue(TransceiverFactory::TX_audio_source_front))
.value<TransceiverFactory::TXAudioSource>();
rig_params_.ptt_port = settings_->value("PTTport").toString();
data_mode_ =
settings_->value("DataMode", QVariant::fromValue(data_mode_none))
.value<Configuration::DataMode>();
insert_blank_ = settings_->value("InsertBlank", false).toBool();
DXCC_ = settings_->value("DXCCEntity", false).toBool();
ppfx_ = settings_->value("PrincipalPrefix", false).toBool();
miles_ = settings_->value("Miles", false).toBool();
hold_ptt_ = settings_->value("HoldPTT", false).toBool();
avoid_forced_identify_ =
settings_->value("AvoidForcedIdentify", false).toBool();
avoid_allcall_ = settings_->value("AvoidAllcall", false).toBool();
spellcheck_ = settings_->value("Spellcheck", true).toBool();
heartbeat_ = settings_->value("TxBeacon", 30).toInt();
watchdog_ = settings_->value("TxIdleWatchdog", 60).toInt();
if (watchdog_) {
watchdog_ = qMax(5, watchdog_);
}
TX_messages_ = settings_->value("Tx2QSO", true).toBool();
rig_params_.poll_interval = settings_->value("Polling", 0).toInt();
rig_params_.split_mode =
settings_
->value("SplitMode",
QVariant::fromValue(TransceiverFactory::split_mode_none))
.value<TransceiverFactory::SplitMode>();
opCall_ = settings_->value("OpCall", "").toString();
ptt_command_ = settings_->value("PTTCommand", "").toString();
aprs_server_name_ =
settings_->value("aprsServer", "rotate.aprs2.net").toString();
aprs_server_port_ = settings_->value("aprsServerPort", 14580).toUInt();
udp_server_name_ = settings_->value("UDPServer", "127.0.0.1").toString();
udp_server_port_ = settings_->value("UDPServerPort", 2242).toUInt();
// WSJT-X Protocol settings
wsjtx_protocol_enabled_ =
settings_->value("WSJTXProtocolEnabled", false).toBool();
wsjtx_server_name_ =
settings_->value("WSJTXServer", "127.0.0.1").toString();
wsjtx_server_port_ = settings_->value("WSJTXServerPort", 2237).toUInt();
wsjtx_TTL_ = settings_->value("WSJTXTTL", 1).toInt();
wsjtx_accept_requests_ =
settings_->value("WSJTXAcceptRequests", false).toBool();
wsjtx_interface_names_ =
settings_->value("WSJTXInterfaces", QStringList()).toStringList();
tcp_server_name_ = settings_->value("TCPServer", "127.0.0.1").toString();
tcp_server_port_ = settings_->value("TCPServerPort", 2442).toUInt();
n3fjp_server_name_ =
settings_->value("N3FJPServer", "127.0.0.1").toString();
n3fjp_server_port_ = settings_->value("N3FJPServerPort", 1100).toUInt();
broadcast_to_n3fjp_ = settings_->value("BroadcastToN3FJP", false).toBool();
n1mm_server_name_ = settings_->value("N1MMServer", "127.0.0.1").toString();
n1mm_server_port_ = settings_->value("N1MMServerPort", 2333).toUInt();
broadcast_to_n1mm_ = settings_->value("BroadcastToN1MM", false).toBool();
accept_udp_requests_ =
settings_->value("AcceptUDPRequests", false).toBool();
accept_tcp_requests_ =
settings_->value("AcceptTCPRequests", false).toBool();
udpEnabled_ = settings_->value("UDPEnabled", false).toBool();
tcpEnabled_ = settings_->value("TCPEnabled", false).toBool();
tcpMaxConnections_ = settings_->value("TCPMaxConnections", 1).toInt();
calibration_.intercept =
settings_->value("CalibrationIntercept", 0.).toDouble();
calibration_.slope_ppm =
settings_->value("CalibrationSlopePPM", 0.).toDouble();
pwrBandTxMemory_ = settings_->value("pwrBandTxMemory", false).toBool();
pwrBandTuneMemory_ = settings_->value("pwrBandTuneMemory", false).toBool();
// notifications
enable_notifications_ =
settings_->value("EnableNotifications", false).toBool();
notifications_enabled_.clear();
notifications_paths_.clear();
settings_->beginGroup("Notifications");
{
foreach (auto group, settings_->childGroups()) {
settings_->beginGroup(group);
{
notifications_enabled_[group] =
settings_->value("enabled", QVariant(false)).toBool();
notifications_paths_[group] =
settings_->value("path", QVariant("")).toString();
}
settings_->endGroup();
}
}
settings_->endGroup();
}
void Configuration::impl::find_audio_devices()
{
// Default true: let's give users access to all available inputs
// Later we can add a checkbox to the UI to toggle this if it bothers people
const bool allow_monitor_inputs =
settings_->value("AllowMonitorInputs", false).toBool();
// INPUT
{
AudioBinding b;
b.mode = QAudioDevice::Input;
b.device_combo = ui_->sound_input_combo_box;
b.channel_combo = ui_->sound_input_channel_combo_box;
b.allow_both = false;
b.allow_monitor_inputs = allow_monitor_inputs;
b.key_id = "SoundInId";
b.key_name = "SoundInName";
b.key_channel = "AudioInputChannel";
resolve_audio_binding(b, next_audio_input_device_, &next_audio_input_channel_);
}
// OUTPUT
{
AudioBinding b;
b.mode = QAudioDevice::Output;
b.device_combo = ui_->sound_output_combo_box;
b.channel_combo = ui_->sound_output_channel_combo_box;
b.allow_both = true;
b.allow_monitor_inputs = true; // irrelevant for outputs
b.key_id = "SoundOutId";
b.key_name = "SoundOutName";
b.key_channel = "AudioOutputChannel";
resolve_audio_binding(b, next_audio_output_device_, &next_audio_output_channel_);
}
// NOTIFICATION OUTPUT
{
AudioBinding b;
b.mode = QAudioDevice::Output;
b.device_combo = ui_->notification_sound_output_combo_box;
b.channel_combo = nullptr;
b.allow_both = false;
b.allow_monitor_inputs = true;
b.key_id = "NotificationSoundOutId";
b.key_name = "NotificationSoundOutName";
b.key_channel = nullptr;
resolve_audio_binding(b, next_notification_audio_output_device_, nullptr);
}
}
void Configuration::impl::resolve_audio_binding(AudioBinding const& b,
QAudioDevice& next_device,
AudioDevice::Channel* next_channel)
{
auto set_if_changed = [&](char const* key, QVariant const& value) {
if (settings_->value(key) != value)
settings_->setValue(key, value);
};
auto ensure_valid_index = [](QComboBox* cb) {
if (!cb) return -1;
int idx = cb->currentIndex();
if (idx < 0 && cb->count() > 0) {
cb->setCurrentIndex(0);
idx = 0;
}
return idx;
};
const QString saved_id = b.key_id ? settings_->value(b.key_id).toString() : QString{};
const QString saved_name = b.key_name ? settings_->value(b.key_name).toString() : QString{};
const bool need_resolve =
(!saved_id.isEmpty() && dev_id(next_device) != saved_id) ||
(saved_id.isEmpty() && next_device.isNull());
if (need_resolve) {
next_device = find_audio_device(b.mode,
b.device_combo,
saved_id,
saved_name,
b.allow_monitor_inputs);
if (!next_device.isNull() && b.key_id)
set_if_changed(b.key_id, next_device.id());
}
if (b.key_channel && b.channel_combo && next_channel) {
*next_channel = AudioDevice::fromString(
settings_->value(b.key_channel, "Mono").toString());
const int idx = ensure_valid_index(b.device_combo);
if (idx >= 0) {
update_audio_channels(b.device_combo,
b.channel_combo,
idx,
b.allow_both);
}
b.channel_combo->setCurrentIndex(static_cast<int>(*next_channel));
}
}
void Configuration::impl::write_settings() {
SettingsGroup g{settings_, "Configuration"};
settings_->setValue("AutoSwitchBands", auto_switch_bands_);
settings_->setValue("MyCall", my_callsign_);
settings_->setValue("MyGrid", my_grid_);
settings_->setValue("MyGroups", my_groups_);
settings_->setValue("AutoWhitelist", auto_whitelist_);
settings_->setValue("AutoBlacklist", auto_blacklist_);
settings_->setValue("HBBlacklist", hb_blacklist_);
settings_->setValue("HBRateLimit", hb_rate_limit_);
settings_->setValue("SpotBlacklist", spot_blacklist_);
settings_->setValue("RXCallsignBlocklist", rx_callsign_blocklist_);
settings_->setValue("PrimaryHighlightWords", primary_highlight_words_);
settings_->setValue("SecondaryHighlightWords", secondary_highlight_words_);
settings_->setValue("EOTCharacter", eot_);
settings_->setValue("MFICharacter", mfi_);
settings_->setValue("MyInfo", my_info_);
settings_->setValue("MyStatus", my_status_);
settings_->setValue("CQMessage", cq_);
settings_->setValue("HBMessage", hb_);
settings_->setValue("Reply", reply_);
settings_->setValue("CallsignAging", callsign_aging_);
settings_->setValue("ActivityAging", activity_aging_);
settings_->setValue("colorCQ", color_cq_);
settings_->setValue("colorPrimary", color_primary_highlight_);
settings_->setValue("colorSecondary", color_secondary_highlight_);
settings_->setValue("colorMyCall", color_mycall_);
settings_->setValue("color_rx_background", color_rx_background_);
settings_->setValue("color_rx_foreground", color_rx_foreground_);
settings_->setValue("color_compose_background", color_compose_background_);
settings_->setValue("color_compose_foreground", color_compose_foreground_);
settings_->setValue("color_tx_foreground", color_tx_foreground_);
settings_->setValue("colorDXCC", color_DXCC_);
settings_->setValue("colorNewCall", color_NewCall_);
settings_->setValue("colorTableBackground", color_table_background_);
settings_->setValue("colorTableHighlight", color_table_highlight_);
settings_->setValue("colorTableForeground", color_table_foreground_);
settings_->setValue("PillsEnabled", pills_enabled_);
settings_->setValue("colorPillRecipientBg", color_pill_recipient_bg_);
settings_->setValue("colorPillRecipientFg", color_pill_recipient_fg_);
settings_->setValue("colorPillCommandBg", color_pill_command_bg_);
settings_->setValue("colorPillCommandFg", color_pill_command_fg_);
settings_->setValue("colorPillGroupBg", color_pill_group_bg_);
settings_->setValue("colorPillGroupFg", color_pill_group_fg_);
settings_->setValue("colorPillSenderBg", color_pill_sender_bg_);
settings_->setValue("colorPillSenderFg", color_pill_sender_fg_);
settings_->setValue("Font", font_.toString());
settings_->setValue("RXTextFont", rx_text_font_.toString());
settings_->setValue("TXTextFont", tx_text_font_.toString());
settings_->setValue("composeTextFont", compose_text_font_.toString());
settings_->setValue("tableFont", table_font_.toString());
settings_->setValue("TxDelay", txDelay_);
settings_->setValue("PTTMethod", QVariant::fromValue(rig_params_.ptt_type));
settings_->setValue("PTTport", rig_params_.ptt_port);
settings_->setValue("SaveDir", save_directory_.absolutePath());
if (!audio_input_device_.isNull()) {
settings_->setValue("SoundInId", audio_input_device_.id());
settings_->setValue("SoundInName", audio_input_device_.description()); // legacy/migration hint
settings_->setValue("AudioInputChannel", AudioDevice::toString(audio_input_channel_));
}
if (!audio_output_device_.isNull()) {
settings_->setValue("SoundOutId", audio_output_device_.id());
settings_->setValue("SoundOutName", audio_output_device_.description()); // legacy/migration hint
settings_->setValue("AudioOutputChannel", AudioDevice::toString(audio_output_channel_));
}
if (!notification_audio_output_device_.isNull()) {
settings_->setValue("NotificationSoundOutId", notification_audio_output_device_.id());
settings_->setValue("NotificationSoundOutName", notification_audio_output_device_.description()); // legacy/migration hint
}
settings_->setValue("TransmitDirected", transmit_directed_);
settings_->setValue("AutoreplyOnAtStartup", autoreply_on_at_startup_);
settings_->setValue("AutoreplyConfirmation", autoreply_confirmation_);
settings_->setValue("BeaconAnywhere", heartbeat_anywhere_);
settings_->setValue("HeartbeatQSOPause", heartbeat_qso_pause_);
settings_->setValue("HeartbeatAckSNR", heartbeat_ack_snr_);
settings_->setValue("RelayOFF", relay_disabled_);
settings_->setValue("MonitorOFF", monitor_off_at_startup_);
settings_->setValue("TransmitOFF", transmit_off_at_startup_);
settings_->setValue("MonitorLastUsed", monitor_last_used_);
settings_->setValue("PSKReporter", spot_to_reporting_networks_);
settings_->setValue("SpotToAPRS", spot_to_aprs_);
settings_->setValue("WriteLogs", write_logs_);
settings_->setValue("ResetActivity", reset_activity_);
settings_->setValue("CheckForUpdates", check_for_updates_);
settings_->setValue("TxQSYAllowed", tx_qsy_allowed_);
settings_->setValue("Macros", macros_.stringList());
settings_->setValue(versionedFrequenciesSettingsKey,
QVariant::fromValue(frequencies_.frequency_list()));
settings_->setValue("stations",
QVariant::fromValue(stations_.station_list()));
settings_->setValue("Rig", rig_params_.rig_name);
settings_->setValue("CATNetworkPort", rig_params_.network_port);
settings_->setValue("CATUSBPort", rig_params_.usb_port);
settings_->setValue("CATSerialPort", rig_params_.serial_port);
settings_->setValue("CATSerialRate", rig_params_.baud);
settings_->setValue("CATDataBits",
QVariant::fromValue(rig_params_.data_bits));
settings_->setValue("CATStopBits",
QVariant::fromValue(rig_params_.stop_bits));
settings_->setValue("CATHandshake",
QVariant::fromValue(rig_params_.handshake));
settings_->setValue("DataMode", QVariant::fromValue(data_mode_));
settings_->setValue("InsertBlank", insert_blank_);
settings_->setValue("DXCCEntity", DXCC_);
settings_->setValue("PrincipalPrefix", ppfx_);
settings_->setValue("Miles", miles_);
settings_->setValue("HoldPTT", hold_ptt_);
settings_->setValue("AvoidForcedIdentify", avoid_forced_identify_);
settings_->setValue("AvoidAllcall", avoid_allcall_);
settings_->setValue("Spellcheck", spellcheck_);
settings_->setValue("TxBeacon", heartbeat_);
settings_->setValue("TxIdleWatchdog", watchdog_);
settings_->setValue("Tx2QSO", TX_messages_);
settings_->setValue("CATForceDTR", rig_params_.force_dtr);
settings_->setValue("DTR", rig_params_.dtr_high);
settings_->setValue("CATForceRTS", rig_params_.force_rts);
settings_->setValue("RTS", rig_params_.rts_high);
settings_->setValue("TXAudioSource",
QVariant::fromValue(rig_params_.audio_source));
settings_->setValue("Polling", rig_params_.poll_interval);
settings_->setValue("SplitMode",
QVariant::fromValue(rig_params_.split_mode));
settings_->setValue("OpCall", opCall_);
settings_->setValue("PTTCommand", ptt_command_);
settings_->setValue("aprsServer", aprs_server_name_);
settings_->setValue("aprsServerPort", aprs_server_port_);
settings_->setValue("APRSSpotting", spot_to_aprs_);
settings_->setValue("APRSISRelay", spot_to_aprs_relay_);
settings_->setValue("UDPServer", udp_server_name_);
settings_->setValue("UDPServerPort", udp_server_port_);
// WSJT-X Protocol settings
settings_->setValue("WSJTXProtocolEnabled", wsjtx_protocol_enabled_);
settings_->setValue("WSJTXServer", wsjtx_server_name_);
settings_->setValue("WSJTXServerPort", wsjtx_server_port_);
settings_->setValue("WSJTXTTL", wsjtx_TTL_);
settings_->setValue("WSJTXAcceptRequests", wsjtx_accept_requests_);
settings_->setValue("WSJTXInterfaces", wsjtx_interface_names_);
settings_->setValue("TCPServer", tcp_server_name_);
settings_->setValue("TCPServerPort", tcp_server_port_);
settings_->setValue("N3FJPServer", n3fjp_server_name_);
settings_->setValue("N3FJPServerPort", n3fjp_server_port_);
settings_->setValue("BroadcastToN3FJP", broadcast_to_n3fjp_);
settings_->setValue("N1MMServer", n1mm_server_name_);
settings_->setValue("N1MMServerPort", n1mm_server_port_);
settings_->setValue("BroadcastToN1MM", broadcast_to_n1mm_);
settings_->setValue("AcceptUDPRequests", accept_udp_requests_);
settings_->setValue("AcceptTCPRequests", accept_tcp_requests_);
settings_->setValue("UDPEnabled", udpEnabled_);
settings_->setValue("TCPEnabled", tcpEnabled_);
settings_->setValue("TCPMaxConnections", tcpMaxConnections_);
settings_->setValue("CalibrationIntercept", calibration_.intercept);
settings_->setValue("CalibrationSlopePPM", calibration_.slope_ppm);
settings_->setValue("pwrBandTxMemory", pwrBandTxMemory_);
settings_->setValue("pwrBandTuneMemory", pwrBandTuneMemory_);
settings_->setValue("Region", QVariant::fromValue(region_));
settings_->setValue("AutoGrid", use_dynamic_info_);
// notifications
settings_->setValue("EnableNotifications", enable_notifications_);
settings_->beginGroup("Notifications");
{
foreach (auto key, notifications_enabled_.keys()) {
settings_->beginGroup(key);
{
settings_->setValue(
"enabled",
QVariant(notifications_enabled_.value(key, false)));
settings_->setValue(
"path", QVariant(notifications_paths_.value(key, "")));
}
settings_->endGroup();
}
}
settings_->endGroup();
}
void Configuration::impl::set_rig_invariants() {
auto const &rig = ui_->rig_combo_box->currentText();
auto const &ptt_port = ui_->PTT_port_combo_box->currentText();
auto ptt_method = static_cast<TransceiverFactory::PTTMethod>(
ui_->PTT_method_button_group->checkedId());
auto CAT_PTT_enabled = transceiver_factory_.has_CAT_PTT(rig);
auto CAT_indirect_serial_PTT =
transceiver_factory_.has_CAT_indirect_serial_PTT(rig);
auto asynchronous_CAT = transceiver_factory_.has_asynchronous_CAT(rig);
auto is_hw_handshake =
ui_->CAT_handshake_group_box->isEnabled() &&
TransceiverFactory::handshake_hardware ==
static_cast<TransceiverFactory::Handshake>(
ui_->CAT_handshake_button_group->checkedId());
ui_->test_CAT_push_button->setStyleSheet({});
ui_->CAT_poll_interval_label->setEnabled(!asynchronous_CAT);
ui_->CAT_poll_interval_spin_box->setEnabled(!asynchronous_CAT);
auto port_type = transceiver_factory_.CAT_port_type(rig);
bool is_serial_CAT(TransceiverFactory::Capabilities::serial == port_type);
auto const &cat_port = ui_->CAT_port_combo_box->currentText();
// only enable CAT option if transceiver has CAT PTT
ui_->PTT_CAT_radio_button->setEnabled(CAT_PTT_enabled);
auto enable_ptt_port = TransceiverFactory::PTT_method_CAT != ptt_method &&
TransceiverFactory::PTT_method_VOX != ptt_method;
ui_->PTT_port_combo_box->setEnabled(enable_ptt_port);
// if PTT port is not enabled, fill it with the text of the CAT port
if (!enable_ptt_port) {
ui_->PTT_port_combo_box->lineEdit()->setText(
ui_->CAT_port_combo_box->currentText());
}
ui_->PTT_port_label->setEnabled(enable_ptt_port);
dynamic_cast<QStandardItemModel *>(ui_->PTT_port_combo_box->model())
->item(ui_->PTT_port_combo_box->findText("CAT"))
->setEnabled(CAT_indirect_serial_PTT);
if (!CAT_indirect_serial_PTT) {
if ("CAT" == ui_->PTT_port_combo_box->currentText() &&
ui_->PTT_port_combo_box->currentIndex() > 0) {
ui_->PTT_port_combo_box->setCurrentIndex(
ui_->PTT_port_combo_box->currentIndex() - 1);
}
}
ui_->PTT_RTS_radio_button->setEnabled(
!(is_serial_CAT && ptt_port == cat_port && is_hw_handshake));
if (TransceiverFactory::basic_transceiver_name_ == rig) {
// makes no sense with rig as "None"
ui_->monitor_last_used_check_box->setEnabled(false);
ui_->catTab->setEnabled(false);
// ui_->CAT_control_group_box->setEnabled (false);
ui_->test_CAT_push_button->setEnabled(false);
ui_->test_PTT_push_button->setEnabled(
TransceiverFactory::PTT_method_DTR == ptt_method ||
TransceiverFactory::PTT_method_RTS == ptt_method);
ui_->TX_audio_source_group_box->setEnabled(false);
} else {
ui_->monitor_last_used_check_box->setEnabled(true);
ui_->catTab->setEnabled(true);
// ui_->CAT_control_group_box->setEnabled (true);
ui_->test_CAT_push_button->setEnabled(true);
ui_->test_PTT_push_button->setEnabled(false);
ui_->TX_audio_source_group_box->setEnabled(
transceiver_factory_.has_CAT_PTT_mic_data(rig) &&
TransceiverFactory::PTT_method_CAT == ptt_method);
if (port_type != last_port_type_) {
last_port_type_ = port_type;
switch (port_type) {
case TransceiverFactory::Capabilities::serial:
fill_port_combo_box(ui_->CAT_port_combo_box);
ui_->CAT_port_combo_box->setCurrentText(
rig_params_.serial_port);
if (ui_->CAT_port_combo_box->currentText().isEmpty() &&
ui_->CAT_port_combo_box->count()) {
ui_->CAT_port_combo_box->setCurrentText(
ui_->CAT_port_combo_box->itemText(0));
}
ui_->CAT_port_label->setText(tr("Serial Port:"));
ui_->CAT_port_combo_box->setToolTip(
tr("Serial port used for CAT control"));
ui_->CAT_port_combo_box->setEnabled(true);
break;
case TransceiverFactory::Capabilities::network:
ui_->CAT_port_combo_box->clear();
ui_->CAT_port_combo_box->setCurrentText(
rig_params_.network_port);
ui_->CAT_port_label->setText(tr("Network Server:"));
ui_->CAT_port_combo_box->setToolTip(
tr("Optional hostname and port of network service.\n"
"Leave blank for a sensible default on this machine.\n"
"Formats:\n"
"\thostname:port\n"
"\tIPv4-address:port\n"
"\t[IPv6-address]:port"));
ui_->CAT_port_combo_box->setEnabled(true);
break;
case TransceiverFactory::Capabilities::usb:
ui_->CAT_port_combo_box->clear();
ui_->CAT_port_combo_box->setCurrentText(rig_params_.usb_port);
ui_->CAT_port_label->setText(tr("USB Device:"));
ui_->CAT_port_combo_box->setToolTip(
tr("Optional device identification.\n"
"Leave blank for a sensible default for the rig.\n"
"Format:\n"
"\t[VID[:PID[:VENDOR[:PRODUCT]]]]"));
ui_->CAT_port_combo_box->setEnabled(true);
break;
default:
ui_->CAT_port_combo_box->clear();
ui_->CAT_port_combo_box->setEnabled(false);
break;
}
}
ui_->CAT_serial_port_parameters_group_box->setEnabled(is_serial_CAT);
ui_->force_DTR_combo_box->setEnabled(
is_serial_CAT &&
(cat_port != ptt_port || !ui_->PTT_DTR_radio_button->isEnabled() ||
!ui_->PTT_DTR_radio_button->isChecked()));
ui_->force_RTS_combo_box->setEnabled(
is_serial_CAT && !is_hw_handshake &&
(cat_port != ptt_port || !ui_->PTT_RTS_radio_button->isEnabled() ||
!ui_->PTT_RTS_radio_button->isChecked()));
}
ui_->mode_group_box->setEnabled(
WSJT_RIG_NONE_CAN_SPLIT ||
TransceiverFactory::basic_transceiver_name_ != rig);
ui_->split_operation_group_box->setEnabled(
WSJT_RIG_NONE_CAN_SPLIT ||
TransceiverFactory::basic_transceiver_name_ != rig);
}
QStringList splitGroups(QString groupsString, bool filter) {
QStringList groups;
if (groupsString.isEmpty()) {
return groups;
}
foreach (QString group, groupsString.split(",")) {
auto g = group.trimmed();
if (filter && !g.startsWith("@")) {
continue;
}
groups.append(group.trimmed().toUpper());
}
return groups;
}
QStringList splitWords(QString callsString) {
QStringList calls;
if (callsString.isEmpty()) {
return calls;
}
foreach (QString call, callsString.split(",")) {
auto g = call.trimmed();
calls.append(call.trimmed().toUpper());
}
return calls;
}
bool Configuration::impl::validate() {
if (ui_->eot_line_edit->text().trimmed().left(2).isEmpty()) {
JS8MessageBox::critical_message(
this, tr("Please enter an end of transmission character"));
return false;
}
auto callsign = ui_->callsign_line_edit->text().toUpper().trimmed();
if (!Varicode::isValidCallsign(callsign, nullptr) ||
callsign.startsWith("@")) {
JS8MessageBox::critical_message(
this, tr("The callsign format you provided is not supported"));
return false;
}
if (!ui_->grid_line_edit->hasAcceptableInput()) {
JS8MessageBox::critical_message(
this, tr("The grid you provided is not valid"));
return false;
}
foreach (
auto group,
splitGroups(ui_->groups_line_edit->text().toUpper().trimmed(), false)) {
if (!Varicode::isGroupAllowed(group)) {
JS8MessageBox::critical_message(
this,
QString("%1 is a group that cannot be joined").arg(group));
return false;
}
if (!Varicode::isCompoundCallsign(group)) {
JS8MessageBox::critical_message(
this, QString("%1 is not a valid group").arg(group));
return false;
}
}
foreach (
auto call,
splitWords(ui_->auto_whitelist_line_edit->text().toUpper().trimmed())) {
if (!Varicode::isValidCallsign(call, nullptr)) {
JS8MessageBox::critical_message(
this,
QString("%1 is not a valid callsign to whitelist").arg(call));
return false;
}
}
auto hb = ui_->hb_message_line_edit->text().toUpper().trimmed();
if (!hb.isEmpty() && !(hb.startsWith("HB") || hb.contains(callsign))) {
JS8MessageBox::critical_message(
this, QString("The HB message format is invalid. It must either "
"start with \"HB\" or contain your callsign."));
return false;
}
auto cq = ui_->cq_message_line_edit->text().toUpper().trimmed();
if (!cq.isEmpty() && !(cq.startsWith("CQ") || cq.contains(callsign))) {
JS8MessageBox::critical_message(
this, QString("The CQ message format is invalid. It must either "
"start with \"CQ\" or contain your callsign."));
return false;
}
if ((ui_->sound_input_combo_box->currentIndex() < 0) &&
next_audio_input_device_.isNull()) {
find_tab(ui_->sound_input_combo_box);
JS8MessageBox::critical_message(this, tr("Invalid audio input device"));
return false;
}
if ((ui_->sound_input_channel_combo_box->currentIndex() < 0) &&
next_audio_input_device_.isNull()) {
find_tab(ui_->sound_input_combo_box);
JS8MessageBox::critical_message(this, tr("Invalid audio input device"));
return false;
}
if ((ui_->sound_output_combo_box->currentIndex() < 0) &&
next_audio_output_device_.isNull()) {
find_tab(ui_->sound_output_combo_box);
JS8MessageBox::information_message(this,
tr("Invalid audio output device"));
// don't reject as we can work without an audio output
}
if ((ui_->notification_sound_output_combo_box->currentIndex() < 0) &&
next_notification_audio_output_device_.isNull()) {
find_tab(ui_->notification_sound_output_combo_box);
JS8MessageBox::information_message(
this, tr("Invalid notification audio output device"));
// don't reject as we can work without a notification audio output
}
if (!ui_->PTT_method_button_group->checkedButton()->isEnabled()) {
JS8MessageBox::critical_message(this, tr("Invalid PTT method"));
return false;
}
auto ptt_method = static_cast<TransceiverFactory::PTTMethod>(
ui_->PTT_method_button_group->checkedId());
if (ptt_method == TransceiverFactory::PTT_method_DTR ||
ptt_method == TransceiverFactory::PTT_method_RTS) {
const auto ptt_port = ui_->PTT_port_combo_box->currentText();
auto *model = dynamic_cast<QStandardItemModel *>(ui_->PTT_port_combo_box->model());
const int index = ui_->PTT_port_combo_box->findText(ptt_port);
QStandardItem *item = (index >= 0 && model) ? model->item(index) : nullptr;
const bool enabled = item && item->isEnabled();
const bool invalid = ptt_port.isEmpty() || !enabled;
if (invalid) {
JS8MessageBox::critical_message(this, tr("Invalid PTT port"));
return false;
}
}
return true;
}
int Configuration::impl::exec() {
// macros can be modified in the main window
next_macros_.setStringList(macros_.stringList());
have_rig_ = rig_active_; // record that we started with a rig open
saved_rig_params_ = rig_params_; // used to detect changes that
// require the Transceiver to be
// re-opened
rig_changed_ = false;
initialize_models();
return QDialog::exec();
}
TransceiverFactory::ParameterPack Configuration::impl::gather_rig_data() const {
TransceiverFactory::ParameterPack result;
result.rig_name = ui_->rig_combo_box->currentText();
switch (transceiver_factory_.CAT_port_type(result.rig_name)) {
case TransceiverFactory::Capabilities::network:
result.network_port = ui_->CAT_port_combo_box->currentText();
result.usb_port = rig_params_.usb_port;
result.serial_port = rig_params_.serial_port;
break;
case TransceiverFactory::Capabilities::usb:
result.usb_port = ui_->CAT_port_combo_box->currentText();
result.network_port = rig_params_.network_port;
result.serial_port = rig_params_.serial_port;
break;
default:
result.serial_port = ui_->CAT_port_combo_box->currentText();
result.network_port = rig_params_.network_port;
result.usb_port = rig_params_.usb_port;
break;
}
result.baud = ui_->CAT_serial_baud_combo_box->currentText().toInt();
result.data_bits = static_cast<TransceiverFactory::DataBits>(
ui_->CAT_data_bits_button_group->checkedId());
result.stop_bits = static_cast<TransceiverFactory::StopBits>(
ui_->CAT_stop_bits_button_group->checkedId());
result.handshake = static_cast<TransceiverFactory::Handshake>(
ui_->CAT_handshake_button_group->checkedId());
result.force_dtr = ui_->force_DTR_combo_box->isEnabled() &&
ui_->force_DTR_combo_box->currentIndex() > 0;
result.dtr_high = ui_->force_DTR_combo_box->isEnabled() &&
1 == ui_->force_DTR_combo_box->currentIndex();
result.force_rts = ui_->force_RTS_combo_box->isEnabled() &&
ui_->force_RTS_combo_box->currentIndex() > 0;
result.rts_high = ui_->force_RTS_combo_box->isEnabled() &&
1 == ui_->force_RTS_combo_box->currentIndex();
result.poll_interval = ui_->CAT_poll_interval_spin_box->value();
result.ptt_type = static_cast<TransceiverFactory::PTTMethod>(
ui_->PTT_method_button_group->checkedId());
// don't allow CAT for None rig
if (result.rig_name == "None" &&
result.ptt_type == TransceiverFactory::PTT_method_CAT) {
result.ptt_type = TransceiverFactory::PTT_method_VOX;
}
result.ptt_port = ui_->PTT_port_combo_box->currentText();
result.audio_source = static_cast<TransceiverFactory::TXAudioSource>(
ui_->TX_audio_source_button_group->checkedId());
result.split_mode = static_cast<TransceiverFactory::SplitMode>(
ui_->split_mode_button_group->checkedId());
return result;
}
void Configuration::impl::accept() {
// Called when OK button is clicked.
if (!validate()) {
return; // not accepting
}
// extract all rig related configuration parameters into temporary
// structure for checking if the rig needs re-opening without
// actually updating our live state
auto temp_rig_params = gather_rig_data();
// open_rig() uses values from models so we use it to validate the
// Transceiver settings before agreeing to accept the configuration
if (temp_rig_params != rig_params_ && !open_rig()) {
return; // not accepting
}
QDialog::accept(); // do this before accessing custom
// models so that any changes in
// delegates in views get flushed to
// the underlying models before we
// access them
sync_transceiver(true); // force an update
//
// from here on we are bound to accept the new configuration
// parameters so extract values from models and make them live
//
if (next_font_ != font_) {
font_ = next_font_;
Q_EMIT self_->gui_text_font_changed(font_);
}
if (next_tx_text_font_ != tx_text_font_) {
tx_text_font_ = next_tx_text_font_;
Q_EMIT self_->tx_text_font_changed(tx_text_font_);
}
if (next_rx_text_font_ != rx_text_font_) {
rx_text_font_ = next_rx_text_font_;
Q_EMIT self_->rx_text_font_changed(rx_text_font_);
}
if (next_compose_text_font_ != compose_text_font_) {
compose_text_font_ = next_compose_text_font_;
Q_EMIT self_->compose_text_font_changed(compose_text_font_);
}
if (next_table_font_ != table_font_) {
table_font_ = next_table_font_;
Q_EMIT self_->table_font_changed(table_font_);
}
color_primary_highlight_ = next_color_primary_highlight_;
color_secondary_highlight_ = next_color_secondary_highlight_;
color_cq_ = next_color_cq_;
color_mycall_ = next_color_mycall_;
color_rx_background_ = next_color_rx_background_;
color_rx_foreground_ = next_color_rx_foreground_;
color_compose_background_ = next_color_compose_background_;
color_compose_foreground_ = next_color_compose_foreground_;
color_tx_foreground_ = next_color_tx_foreground_;
color_DXCC_ = next_color_DXCC_;
color_NewCall_ = next_color_NewCall_;
color_table_background_ = next_color_table_background_;
color_table_highlight_ = next_color_table_highlight_;
color_table_foreground_ = next_color_table_foreground_;
pills_enabled_ = ui_->pills_enabled_check_box->isChecked();
color_pill_recipient_bg_ = next_color_pill_recipient_bg_;
color_pill_recipient_fg_ = next_color_pill_recipient_fg_;
color_pill_command_bg_ = next_color_pill_command_bg_;
color_pill_command_fg_ = next_color_pill_command_fg_;
color_pill_group_bg_ = next_color_pill_group_bg_;
color_pill_group_fg_ = next_color_pill_group_fg_;
color_pill_sender_bg_ = next_color_pill_sender_bg_;
color_pill_sender_fg_ = next_color_pill_sender_fg_;
Q_EMIT self_->colors_changed();
rig_params_ = temp_rig_params; // now we can go live with the rig
// related configuration parameters
rig_is_dummy_ =
TransceiverFactory::basic_transceiver_name_ == rig_params_.rig_name;
{
auto const selected_device =
ui_->sound_input_combo_box->currentData().value<QAudioDevice>();
if (dev_id(selected_device) != dev_id(next_audio_input_device_)) {
next_audio_input_device_ = selected_device;
}
}
{
auto const selected_device =
ui_->sound_output_combo_box->currentData().value<QAudioDevice>();
if (dev_id(selected_device) != dev_id(next_audio_output_device_)) {
next_audio_output_device_ = selected_device;
}
}
{
auto const selected_device =
ui_->notification_sound_output_combo_box->currentData().value<QAudioDevice>();
if (dev_id(selected_device) != dev_id(next_notification_audio_output_device_)) {
next_notification_audio_output_device_ = selected_device;
}
}
if (auto const selected_channel = static_cast<AudioDevice::Channel>(
ui_->sound_input_channel_combo_box->currentIndex());
selected_channel != next_audio_input_channel_) {
next_audio_input_channel_ = selected_channel;
}
Q_ASSERT(next_audio_input_channel_ <= AudioDevice::Right);
if (auto const selected_channel = static_cast<AudioDevice::Channel>(
ui_->sound_output_channel_combo_box->currentIndex());
selected_channel != next_audio_output_channel_) {
next_audio_output_channel_ = selected_channel;
}
Q_ASSERT(next_audio_output_channel_ <= AudioDevice::Both);
if (audio_input_device_ != next_audio_input_device_ ||
next_audio_input_device_.isNull()) {
audio_input_device_ = next_audio_input_device_;
restart_sound_input_device_ = true;
}
if (audio_input_channel_ != next_audio_input_channel_) {
audio_input_channel_ = next_audio_input_channel_;
restart_sound_input_device_ = true;
}
if (audio_output_device_ != next_audio_output_device_ ||
next_audio_output_device_.isNull()) {
audio_output_device_ = next_audio_output_device_;
restart_sound_output_device_ = true;
}
if (audio_output_channel_ != next_audio_output_channel_) {
audio_output_channel_ = next_audio_output_channel_;
restart_sound_output_device_ = true;
}
if (notification_audio_output_device_ !=
next_notification_audio_output_device_ ||
next_notification_audio_output_device_.isNull()) {
notification_audio_output_device_ =
next_notification_audio_output_device_;
restart_notification_sound_output_device_ = true;
}
qCDebug(configuration_js8)
<< "Configure::accept: audio i/p:" << audio_input_device_.description()
<< "chan:" << audio_input_channel_
<< "o/p:" << audio_output_device_.description()
<< "chan:" << audio_output_channel_
<< "n:" << notification_audio_output_device_.description()
<< "reset i/p:" << restart_sound_input_device_
<< "reset o/p:" << restart_sound_output_device_
<< "reset n:" << restart_notification_sound_output_device_;
my_callsign_ = ui_->callsign_line_edit->text().toUpper().trimmed();
my_grid_ = ui_->grid_line_edit->text().toUpper().trimmed();
my_groups_ =
splitGroups(ui_->groups_line_edit->text().toUpper().trimmed(), true);
auto_whitelist_ =
splitWords(ui_->auto_whitelist_line_edit->text().toUpper().trimmed());
auto_blacklist_ =
splitWords(ui_->auto_blacklist_line_edit->text().toUpper().trimmed());
hb_blacklist_ =
splitWords(ui_->hb_blacklist_line_edit->text().toUpper().trimmed());
spot_blacklist_ =
splitWords(ui_->spot_blacklist_line_edit->text().toUpper().trimmed());
rx_callsign_blocklist_ =
splitWords(ui_->rx_blocklist_line_edit->text().toUpper().trimmed());
primary_highlight_words_ =
splitWords(ui_->primaryHighlightLineEdit->text().toUpper().trimmed());
secondary_highlight_words_ =
splitWords(ui_->secondaryHighlightLineEdit->text().toUpper().trimmed());
cq_ = ui_->cq_message_line_edit->text().toUpper();
hb_ = ui_->hb_message_line_edit->text().toUpper();
reply_ = ui_->reply_message_line_edit->text().toUpper();
eot_ = ui_->eot_line_edit->text();
mfi_ = ui_->mfi_line_edit->text();
my_info_ = ui_->info_message_line_edit->text().toUpper();
my_status_ = ui_->status_message_line_edit->text().toUpper();
callsign_aging_ = ui_->callsign_aging_spin_box->value();
activity_aging_ = ui_->activity_aging_spin_box->value();
spot_to_reporting_networks_ = ui_->psk_reporter_check_box->isChecked();
spot_to_aprs_ = ui_->enable_aprs_spotting_check_box->isChecked();
txDelay_ = ui_->sbTxDelay->value();
write_logs_ = ui_->write_logs_check_box->isChecked();
reset_activity_ = ui_->reset_activity_check_box->isChecked();
check_for_updates_ = ui_->checkForUpdates_checkBox->isChecked();
tx_qsy_allowed_ = ui_->tx_qsy_check_box->isChecked();
transmit_directed_ = ui_->transmit_directed_check_box->isChecked();
autoreply_on_at_startup_ = ui_->autoreply_on_check_box->isChecked();
autoreply_confirmation_ =
ui_->autoreply_confirmation_check_box->isChecked();
heartbeat_anywhere_ = ui_->heartbeat_anywhere_check_box->isChecked();
heartbeat_qso_pause_ = ui_->heartbeat_qso_pause_check_box->isChecked();
heartbeat_ack_snr_ = ui_->heartbeat_ack_snr_check_box->isChecked();
hb_rate_limit_ = ui_->hb_rate_limit_check_box->isChecked();
relay_disabled_ = ui_->relay_disabled_check_box->isChecked();
monitor_off_at_startup_ = ui_->monitor_off_check_box->isChecked();
transmit_off_at_startup_ = ui_->transmit_off_check_box->isChecked();
monitor_last_used_ = ui_->monitor_last_used_check_box->isChecked();
miles_ = ui_->miles_check_box->isChecked();
hold_ptt_ = ui_->hold_ptt_check_box->isChecked();
avoid_forced_identify_ = ui_->avoid_forced_identify_check_box->isChecked();
avoid_allcall_ = ui_->avoid_allcall_check_box->isChecked();
spellcheck_ = ui_->spellcheck_check_box->isChecked();
heartbeat_ = ui_->heartbeat_spin_box->value();
watchdog_ = ui_->tx_watchdog_spin_box->value();
data_mode_ = static_cast<DataMode>(ui_->TX_mode_button_group->checkedId());
save_directory_.setPath(ui_->save_path_display_label->text());
calibration_.intercept = ui_->calibration_intercept_spin_box->value();
calibration_.slope_ppm = ui_->calibration_slope_ppm_spin_box->value();
pwrBandTxMemory_ = ui_->checkBoxPwrBandTxMemory->isChecked();
pwrBandTuneMemory_ = ui_->checkBoxPwrBandTuneMemory->isChecked();
opCall_ = ui_->opCallEntry->text();
ptt_command_ = ui_->ptt_command_line_edit->text();
aprs_server_name_ = ui_->aprs_server_line_edit->text();
aprs_server_port_ = ui_->aprs_server_port_spin_box->value();
auto const newAutoSwitchBands =
ui_->auto_switch_bands_check_box->isChecked();
auto_switch_bands_ = newAutoSwitchBands;
// Emit this even if the value has not changed as a way to reset the
// scheduler.
// TODO this is smelly, revisit when we make the scheduler smarter
Q_EMIT self_->auto_switch_bands_changed(auto_switch_bands_);
auto const newUdpEnabled = ui_->udpEnable->isChecked();
auto const newUdpServerName = ui_->udp_server_line_edit->text();
if (newUdpServerName != udp_server_name_ || newUdpEnabled != udpEnabled_) {
udp_server_name_ = newUdpServerName;
udpEnabled_ = newUdpEnabled;
Q_EMIT self_->udp_server_name_changed(udpEnabled_ ? newUdpServerName
: "");
}
auto newUdpPort = ui_->udp_server_port_spin_box->value();
if (newUdpPort != udp_server_port_) {
udp_server_port_ = newUdpPort;
Q_EMIT self_->udp_server_port_changed(newUdpPort);
}
auto newTcpEnabled = ui_->tcpEnable->isChecked();
auto newTcpServer = ui_->tcp_server_line_edit->text();
if (newTcpServer != tcp_server_name_ || newTcpEnabled != tcpEnabled_) {
tcp_server_name_ = newTcpServer;
tcpEnabled_ = newTcpEnabled;
Q_EMIT self_->tcp_server_changed(tcpEnabled_ ? newTcpServer : "");
}
auto newTcpPort = ui_->tcp_server_port_spin_box->value();
if (newTcpPort != tcp_server_port_) {
tcp_server_port_ = newTcpPort;
Q_EMIT self_->tcp_server_port_changed(newTcpPort);
}
auto newTcpMaxConnections = ui_->tcp_max_connections_spin_box->value();
if (newTcpMaxConnections != tcpMaxConnections_) {
tcpMaxConnections_ = newTcpMaxConnections;
Q_EMIT self_->tcp_max_connections_changed(newTcpMaxConnections);
}
accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked();
accept_tcp_requests_ = ui_->accept_tcp_requests_check_box->isChecked();
auto new_n3fjp_server = ui_->n3fjp_server_name_line_edit->text();
n3fjp_server_name_ = new_n3fjp_server;
auto new_n3fjp_port = ui_->n3fjp_server_port_spin_box->value();
n3fjp_server_port_ = new_n3fjp_port;
broadcast_to_n3fjp_ = ui_->enable_n3fjp_broadcast_check_box->isChecked();
auto new_n1mm_server = ui_->n1mm_server_name_line_edit->text();
n1mm_server_name_ = new_n1mm_server;
auto new_n1mm_port = ui_->n1mm_server_port_spin_box->value();
n1mm_server_port_ = new_n1mm_port;
broadcast_to_n1mm_ = ui_->enable_n1mm_broadcast_check_box->isChecked();
// WSJT-X Protocol settings
auto new_wsjtx_enabled = ui_->wsjtx_enable_check_box->isChecked();
auto new_wsjtx_server = ui_->wsjtx_server_line_edit->text();
auto new_wsjtx_port = ui_->wsjtx_server_port_spin_box->value();
auto new_wsjtx_ttl = ui_->wsjtx_TTL_spin_box->value();
auto new_wsjtx_accept_requests =
ui_->wsjtx_accept_requests_check_box->isChecked();
auto new_wsjtx_interfaces =
get_selected_network_interfaces(ui_->wsjtx_interfaces_combo_box);
if (new_wsjtx_enabled != wsjtx_protocol_enabled_) {
wsjtx_protocol_enabled_ = new_wsjtx_enabled;
Q_EMIT self_->wsjtx_protocol_enabled_changed(wsjtx_protocol_enabled_);
}
if (new_wsjtx_server != wsjtx_server_name_ ||
new_wsjtx_enabled != wsjtx_protocol_enabled_) {
wsjtx_server_name_ = new_wsjtx_server;
Q_EMIT self_->wsjtx_server_changed(
wsjtx_protocol_enabled_ ? new_wsjtx_server : "");
}
if (new_wsjtx_port != wsjtx_server_port_) {
wsjtx_server_port_ = new_wsjtx_port;
Q_EMIT self_->wsjtx_server_port_changed(new_wsjtx_port);
}
if (new_wsjtx_ttl != wsjtx_TTL_) {
wsjtx_TTL_ = new_wsjtx_ttl;
Q_EMIT self_->wsjtx_TTL_changed(new_wsjtx_ttl);
}
wsjtx_accept_requests_ = new_wsjtx_accept_requests;
if (new_wsjtx_interfaces != wsjtx_interface_names_) {
wsjtx_interface_names_ = new_wsjtx_interfaces;
Q_EMIT self_->wsjtx_interfaces_changed(wsjtx_interface_names_);
}
if (macros_.stringList() != next_macros_.stringList()) {
macros_.setStringList(next_macros_.stringList());
}
region_ = IARURegions::value(ui_->region_combo_box->currentText());
if (frequencies_.frequency_list() != next_frequencies_.frequency_list()) {
frequencies_.frequency_list(next_frequencies_.frequency_list());
frequencies_.sort(FrequencyList_v3::frequency_column);
}
if (stations_.station_list() != next_stations_.station_list()) {
stations_.station_list(next_stations_.station_list());
stations_.sort(StationList::switch_at_column);
Q_EMIT self_->band_schedule_changed(this->stations_);
}
if (ui_->use_dynamic_grid->isChecked() && !use_dynamic_info_) {
// turning on so clear it so only the next location update gets used
dynamic_grid_.clear();
}
use_dynamic_info_ = ui_->use_dynamic_grid->isChecked();
// notifications
enable_notifications_ = ui_->notifications_check_box->isChecked();
for (int i = 0; i < ui_->notifications_table_widget->rowCount(); i++) {
// event
auto eventItem = ui_->notifications_table_widget->item(i, 0);
auto key = eventItem->data(Qt::UserRole).toString();
if (key.isEmpty()) {
continue;
}
auto enabledWidget = ui_->notifications_table_widget->cellWidget(i, 1);
QCheckBox *enabledCheckbox =
enabledWidget->findChild<QCheckBox *>("enabledCheckbox");
if (enabledCheckbox) {
notifications_enabled_[key] = enabledCheckbox->isChecked();
}
auto pathWidget = ui_->notifications_table_widget->cellWidget(i, 2);
QLabel *pathLabel = qobject_cast<QLabel *>(pathWidget);
if (pathLabel) {
notifications_paths_[key] = pathLabel->text();
}
}
write_settings(); // make visible to all
}
void Configuration::impl::reject() {
initialize_models(); // reverts to settings as at exec ()
// check if the Transceiver instance changed, in which case we need
// to re open any prior Transceiver type
if (rig_changed_) {
if (have_rig_) {
// we have to do this since the rig has been opened since we
// were exec'ed even though it might fail
open_rig();
} else {
close_rig();
}
}
QDialog::reject();
}
void Configuration::impl::on_psk_reporter_check_box_toggled(bool checked) {
ui_->enable_aprs_spotting_check_box->setEnabled(checked);
bool spot = ui_->enable_aprs_spotting_check_box->isChecked();
ui_->aprs_server_line_edit->setEnabled(spot && checked);
ui_->aprs_server_port_spin_box->setEnabled(spot && checked);
}
void Configuration::impl::on_enable_aprs_spotting_check_box_toggled(
bool checked) {
ui_->aprs_server_line_edit->setEnabled(checked);
ui_->aprs_server_port_spin_box->setEnabled(checked);
}
void Configuration::impl::on_enable_aprs_relay_check_box_toggled(bool checked) {
spot_to_aprs_relay_ = checked;
Q_EMIT self_->spot_to_aprs_relay_changed(spot_to_aprs_relay_);
}
void Configuration::impl::on_notifications_check_box_toggled(bool checked) {
ui_->notifications_table_widget->setEnabled(checked);
}
void Configuration::impl::on_font_push_button_clicked() {
next_font_ = QFontDialog::getFont(0, next_font_, this, tr("Font Chooser")
#if QT_VERSION >= 0x050201
,
QFontDialog::DontUseNativeDialog
#endif
);
ui_->font_push_button->setText(QString("Application Font (%1 %2)")
.arg(next_font_.family())
.arg(next_font_.pointSize()));
}
void Configuration::impl::on_style_push_button_clicked() {
QString fileName = QFileDialog::getOpenFileName(nullptr,
"Open Stylesheet", "", "Style Sheets (*.qss);;All Files (*)");
if (fileName.isEmpty()) return;
QFile file(fileName);
if (file.open(QFile::ReadOnly | QFile::Text)) {
QTextStream stream(&file);
// Apply the stylesheet globally
qApp->setStyleSheet(stream.readAll());
file.close();
}
ui_->style_push_button->setText(QString("Application Stylesheet (%1)")
.arg(QFileInfo(fileName).fileName()));
}
void Configuration::impl::on_tableFontButton_clicked() {
next_table_font_ =
QFontDialog::getFont(0, next_table_font_, this, tr("Font Chooser")
#if QT_VERSION >= 0x050201
,
QFontDialog::DontUseNativeDialog
#endif
);
ui_->tableFontButton->setText(QString("Font (%1 %2)")
.arg(next_table_font_.family())
.arg(next_table_font_.pointSize()));
}
QColor getColor(QColor initial, QWidget *parent, QString title) {
QList<QColor> custom = {QColor("#66FF66"), QColor("#FF6666"),
QColor("#FFEAA7"), QColor("#3498DB")};
auto d = new QColorDialog(initial, parent);
d->setWindowTitle(title);
for (int i = 0; i < custom.length(); i++) {
d->setCustomColor(i, custom.at(i));
}
if (d->exec() == QColorDialog::Accepted) {
return d->selectedColor();
} else {
return initial;
}
}
void Configuration::impl::updatePillPreview(QLabel *label, const QColor &bg,
const QColor &fg) {
label->setStyleSheet(QString("background: %1; color: %2")
.arg(bg.name())
.arg(fg.name()));
}
void Configuration::impl::choosePillPreviewColor(
QColor &target, QLabel *label, const QColor &bg, const QColor &fg,
const QString &title) {
auto color = getColor(target, this, title);
if (!color.isValid())
return;
target = color;
updatePillPreview(label, bg, fg);
}
void Configuration::impl::on_cqMessagesButton_clicked() {
auto new_color = getColor(next_color_cq_, this, "CQ Messages Color");
if (new_color.isValid()) {
next_color_cq_ = new_color;
ui_->cqMessagesLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_cq_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_primaryHighlightButton_clicked() {
auto new_color = getColor(next_color_primary_highlight_, this,
"Primary Highlight Color");
if (new_color.isValid()) {
next_color_primary_highlight_ = new_color;
ui_->primaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_primary_highlight_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_secondaryHighlightButton_clicked() {
auto new_color = getColor(next_color_secondary_highlight_, this,
"Secondary Highlight Color");
if (new_color.isValid()) {
next_color_secondary_highlight_ = new_color;
ui_->secondaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_secondary_highlight_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_pbMyCall_clicked() {
auto new_color =
getColor(next_color_mycall_, this, "Directed Messages Color");
if (new_color.isValid()) {
next_color_mycall_ = new_color;
ui_->labMyCall->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_mycall_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_tableBackgroundButton_clicked() {
auto new_color =
getColor(next_color_table_background_, this, "Table Background Color");
if (new_color.isValid()) {
next_color_table_background_ = new_color;
ui_->tableForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
ui_->tableBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_tableSelectedRowBackgroundButton_clicked() {
auto new_color = getColor(next_color_table_highlight_, this,
"Table Selected Row Background Color");
if (new_color.isValid()) {
next_color_table_highlight_ = new_color;
ui_->tableSelectionBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_highlight_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_tableForegroundButton_clicked() {
auto new_color =
getColor(next_color_table_foreground_, this, "Table Foreground Color");
if (new_color.isValid()) {
next_color_table_foreground_ = new_color;
ui_->tableForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
ui_->tableBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_background_.name())
.arg(next_color_table_foreground_.name()));
ui_->tableSelectionBackgroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_table_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->primaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_primary_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->secondaryHighlightLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_secondary_highlight_.name())
.arg(next_color_table_foreground_.name()));
ui_->cqMessagesLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_cq_.name())
.arg(next_color_table_foreground_.name()));
ui_->labMyCall->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_mycall_.name())
.arg(next_color_table_foreground_.name()));
}
}
void Configuration::impl::on_rxBackgroundButton_clicked() {
auto new_color = getColor(next_color_rx_background_, this,
"Received Messages Background Color");
if (new_color.isValid()) {
next_color_rx_background_ = new_color;
ui_->rxLabel->setStyleSheet(QString("background: %1; color: %2")
.arg(color_rx_background_.name())
.arg(color_rx_foreground_.name()));
ui_->rxForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_rx_foreground_.name()));
ui_->txForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_tx_foreground_.name()));
}
}
void Configuration::impl::on_rxForegroundButton_clicked() {
auto new_color = getColor(next_color_rx_foreground_, this,
"Received Messages Foreground Color");
if (new_color.isValid()) {
next_color_rx_foreground_ = new_color;
ui_->rxLabel->setStyleSheet(QString("background: %1; color: %2")
.arg(color_rx_background_.name())
.arg(color_rx_foreground_.name()));
ui_->rxForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_rx_foreground_.name()));
ui_->txForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_tx_foreground_.name()));
}
}
void Configuration::impl::on_rxFontButton_clicked() {
next_rx_text_font_ =
QFontDialog::getFont(0, next_rx_text_font_, this, tr("Font Chooser")
#if QT_VERSION >= 0x050201
,
QFontDialog::DontUseNativeDialog
#endif
);
ui_->rxFontButton->setText(QString("Font (%1 %2)")
.arg(next_rx_text_font_.family())
.arg(next_rx_text_font_.pointSize()));
}
void Configuration::impl::on_composeBackgroundButton_clicked() {
auto new_color = getColor(next_color_compose_background_, this,
"Compose Messages Background Color");
if (new_color.isValid()) {
next_color_compose_background_ = new_color;
ui_->composeLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_compose_background_.name())
.arg(next_color_compose_foreground_.name()));
}
}
void Configuration::impl::on_composeForegroundButton_clicked() {
auto new_color = getColor(next_color_compose_foreground_, this,
"Compose Messages Foreground Color");
if (new_color.isValid()) {
next_color_compose_foreground_ = new_color;
ui_->composeLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_compose_background_.name())
.arg(next_color_compose_foreground_.name()));
}
}
void Configuration::impl::on_txForegroundButton_clicked() {
auto new_color = getColor(next_color_tx_foreground_, this,
"Transmitted Messages Foreground Color");
if (new_color.isValid()) {
next_color_tx_foreground_ = new_color;
ui_->rxForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_rx_foreground_.name()));
ui_->txForegroundLabel->setStyleSheet(
QString("background: %1; color: %2")
.arg(next_color_rx_background_.name())
.arg(next_color_tx_foreground_.name()));
}
}
void Configuration::impl::on_txFontButton_clicked() {
next_tx_text_font_ =
QFontDialog::getFont(0, next_tx_text_font_, this, tr("Font Chooser")
#if QT_VERSION >= 0x050201
,
QFontDialog::DontUseNativeDialog
#endif
);
ui_->txFontButton->setText(QString("Font (%1 %2)")
.arg(next_tx_text_font_.family())
.arg(next_tx_text_font_.pointSize()));
}
void Configuration::impl::on_composeFontButton_clicked() {
next_compose_text_font_ = QFontDialog::getFont(
0, next_compose_text_font_, this, tr("Font Chooser")
#if QT_VERSION >= 0x050201
,
QFontDialog::DontUseNativeDialog
#endif
);
ui_->composeFontButton->setText(
QString("Font (%1 %2)")
.arg(next_compose_text_font_.family())
.arg(next_compose_text_font_.pointSize()));
}
void Configuration::impl::on_pillRecipientBgButton_clicked() {
choosePillPreviewColor(next_color_pill_recipient_bg_,
ui_->pillRecipientLabel,
next_color_pill_recipient_bg_,
next_color_pill_recipient_fg_,
QStringLiteral("Recipient Pill Background"));
}
void Configuration::impl::on_pillRecipientFgButton_clicked() {
choosePillPreviewColor(next_color_pill_recipient_fg_,
ui_->pillRecipientLabel,
next_color_pill_recipient_bg_,
next_color_pill_recipient_fg_,
QStringLiteral("Recipient Pill Font"));
}
void Configuration::impl::on_pillCommandBgButton_clicked() {
choosePillPreviewColor(next_color_pill_command_bg_, ui_->pillCommandLabel,
next_color_pill_command_bg_,
next_color_pill_command_fg_,
QStringLiteral("Command Pill Background"));
}
void Configuration::impl::on_pillCommandFgButton_clicked() {
choosePillPreviewColor(next_color_pill_command_fg_, ui_->pillCommandLabel,
next_color_pill_command_bg_,
next_color_pill_command_fg_,
QStringLiteral("Command Pill Font"));
}
void Configuration::impl::on_pillGroupBgButton_clicked() {
choosePillPreviewColor(next_color_pill_group_bg_, ui_->pillGroupLabel,
next_color_pill_group_bg_,
next_color_pill_group_fg_,
QStringLiteral("Group Pill Background"));
}
void Configuration::impl::on_pillGroupFgButton_clicked() {
choosePillPreviewColor(next_color_pill_group_fg_, ui_->pillGroupLabel,
next_color_pill_group_bg_,
next_color_pill_group_fg_,
QStringLiteral("Group Pill Font"));
}
void Configuration::impl::on_pillSenderBgButton_clicked() {
choosePillPreviewColor(next_color_pill_sender_bg_, ui_->pillSenderLabel,
next_color_pill_sender_bg_,
next_color_pill_sender_fg_,
QStringLiteral("Sender Pill Background"));
}
void Configuration::impl::on_pillSenderFgButton_clicked() {
choosePillPreviewColor(next_color_pill_sender_fg_, ui_->pillSenderLabel,
next_color_pill_sender_bg_,
next_color_pill_sender_fg_,
QStringLiteral("Sender Pill Font"));
}
void Configuration::impl::on_PTT_port_combo_box_activated(int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_port_combo_box_activated(int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_serial_baud_combo_box_currentIndexChanged(
int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_handshake_button_group_buttonClicked(
QAbstractButton *) {
set_rig_invariants();
}
void Configuration::impl::on_rig_combo_box_currentIndexChanged(
int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_data_bits_button_group_buttonClicked(
QAbstractButton *) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_stop_bits_button_group_buttonClicked(
QAbstractButton *) {
set_rig_invariants();
}
void Configuration::impl::on_CAT_poll_interval_spin_box_valueChanged(
int /* value */) {
set_rig_invariants();
}
void Configuration::impl::on_split_mode_button_group_buttonClicked(
QAbstractButton *) {
set_rig_invariants();
}
void Configuration::impl::on_test_CAT_push_button_clicked() {
if (!validate()) {
return;
}
ui_->test_CAT_push_button->setStyleSheet({});
if (open_rig(true)) {
// Q_EMIT sync (true);
}
set_rig_invariants();
}
void Configuration::impl::on_test_PTT_push_button_clicked(bool checked) {
ui_->test_PTT_push_button->setChecked(!checked); // let status
// update check us
if (!validate()) {
return;
}
if (rig_active_) {
// Update the data mode from the UI before testing PTT
data_mode_ =
static_cast<DataMode>(ui_->TX_mode_button_group->checkedId());
Q_EMIT self_->transceiver_ptt(checked);
}
}
void Configuration::impl::on_force_DTR_combo_box_currentIndexChanged(
int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_force_RTS_combo_box_currentIndexChanged(
int /* index */) {
set_rig_invariants();
}
void Configuration::impl::on_PTT_method_button_group_buttonClicked(
QAbstractButton *) {
set_rig_invariants();
}
void Configuration::impl::on_groups_line_edit_textChanged(QString const &) {}
void Configuration::impl::on_info_message_line_edit_textChanged(
QString const &) {}
void Configuration::impl::on_cq_message_line_edit_textChanged(QString const &) {
}
void Configuration::impl::on_reply_message_line_edit_textChanged(
QString const &) {}
void Configuration::impl::on_add_macro_line_edit_editingFinished() {
ui_->add_macro_line_edit->setText(
ui_->add_macro_line_edit->text().toUpper());
}
void Configuration::impl::on_delete_macro_push_button_clicked(
bool /* checked */) {
auto selection_model = ui_->macros_list_view->selectionModel();
if (selection_model->hasSelection()) {
// delete all selected items
delete_selected_macros(selection_model->selectedRows());
}
}
void Configuration::impl::delete_macro() {
auto selection_model = ui_->macros_list_view->selectionModel();
if (!selection_model->hasSelection()) {
// delete item under cursor if any
auto index = selection_model->currentIndex();
if (index.isValid()) {
next_macros_.removeRow(index.row());
}
} else {
// delete the whole selection
delete_selected_macros(selection_model->selectedRows());
}
}
void Configuration::impl::delete_selected_macros(
QModelIndexList selected_rows) {
// sort in reverse row order so that we can delete without changing
// indices underneath us
std::sort(selected_rows.begin(), selected_rows.end(),
[](QModelIndex const &lhs, QModelIndex const &rhs) {
return rhs.row() < lhs.row(); // reverse row ordering
});
// now delete them
Q_FOREACH (auto index, selected_rows) {
next_macros_.removeRow(index.row());
}
}
void Configuration::impl::on_add_macro_push_button_clicked(bool /* checked */) {
if (next_macros_.insertRow(next_macros_.rowCount())) {
auto index = next_macros_.index(next_macros_.rowCount() - 1);
ui_->macros_list_view->setCurrentIndex(index);
next_macros_.setData(index, ui_->add_macro_line_edit->text().toUpper());
ui_->add_macro_line_edit->clear();
}
}
void Configuration::impl::delete_frequencies() {
auto selection_model = ui_->frequencies_table_view->selectionModel();
selection_model->select(selection_model->selection(),
QItemSelectionModel::SelectCurrent |
QItemSelectionModel::Rows);
next_frequencies_.removeDisjointRows(selection_model->selectedRows());
ui_->frequencies_table_view->resizeColumnToContents(
FrequencyList_v3::mode_column);
}
void Configuration::impl::load_frequencies() {
auto file_name = QFileDialog::getOpenFileName(
this, tr("Load Working Frequencies"),
writeable_data_dir_.absolutePath(),
tr("Frequency files (*.qrg);;All files (*.*)"));
if (!file_name.isNull()) {
auto const list = read_frequencies_file(file_name);
if (list.size() &&
(!next_frequencies_.frequency_list().size() ||
JS8MessageBox::Yes ==
JS8MessageBox::query_message(
this, tr("Replace Working Frequencies"),
tr("Are you sure you want to discard your current "
"working frequencies and replace them with the "
"loaded ones?")))) {
next_frequencies_.frequency_list(list); // update the model
}
}
}
void Configuration::impl::merge_frequencies() {
auto file_name = QFileDialog::getOpenFileName(
this, tr("Merge Working Frequencies"),
writeable_data_dir_.absolutePath(),
tr("Frequency files (*.qrg);;All files (*.*)"));
if (!file_name.isNull()) {
next_frequencies_.frequency_list_merge(
read_frequencies_file(file_name)); // update the model
}
}
FrequencyList_v3::FrequencyItems
Configuration::impl::read_frequencies_file(QString const &file_name) {
FrequencyList_v3::FrequencyItems list;
QFile frequencies_file{file_name};
bool opened = frequencies_file.open(QFile::ReadOnly);
if (!opened) {
JS8MessageBox::warning_message(this, tr("Unable to open frequencies file"));
return list;
}
QDataStream ids{&frequencies_file};
quint32 magic;
ids >> magic;
if (qrg_magic != magic) {
JS8MessageBox::warning_message(this, tr("Not a valid frequencies file"),
tr("Incorrect file magic"));
return list;
}
quint32 version;
ids >> version;
// handle version checks and QDataStream version here if
// necessary
if (version > qrg_version) {
JS8MessageBox::warning_message(this, tr("Not a valid frequencies file"),
tr("Version is too new"));
return list;
}
// de-serialize the data using version if necessary to
// handle old schemata
if (version == 102) {
FrequencyList_v2::FrequencyItems v2list;
ids >> v2list;
list.reserve(v2list.size());
for (const auto& it : v2list) {
FrequencyList_v3::Item n;
n.frequency_ = it.frequency_;
n.mode_ = it.mode_;
n.region_ = it.region_;
n.description_.clear();
list.push_back(std::move(n));
}
}
else {
ids >> list;
}
if (ids.status() != QDataStream::Ok || !ids.atEnd()) {
JS8MessageBox::warning_message(this, tr("Not a valid frequencies file"),
tr("Contents corrupt"));
list.clear();
return list;
}
return list;
}
void Configuration::impl::save_frequencies() {
auto file_name = QFileDialog::getSaveFileName(
this, tr("Save Working Frequencies"),
writeable_data_dir_.absolutePath(),
tr("Frequency files (*.qrg);;All files (*.*)"));
if (!file_name.isNull()) {
QFile frequencies_file{file_name};
bool opened = frequencies_file.open(QFile::WriteOnly);
if (!opened) {
JS8MessageBox::warning_message(this, tr("Unable to open frequencies file"));
return;
}
QDataStream ods{&frequencies_file};
auto selection_model = ui_->frequencies_table_view->selectionModel();
if (selection_model->hasSelection() &&
JS8MessageBox::Yes ==
JS8MessageBox::query_message(
this, tr("Only Save Selected Working Frequencies"),
tr("Are you sure you want to save only the "
"working frequencies that are currently selected? "
"Click No to save all."))) {
selection_model->select(selection_model->selection(),
QItemSelectionModel::SelectCurrent |
QItemSelectionModel::Rows);
ods << qrg_magic << qrg_version
<< next_frequencies_.frequency_list(
selection_model->selectedRows());
} else {
ods << qrg_magic << qrg_version
<< next_frequencies_.frequency_list();
}
}
}
void Configuration::impl::reset_frequencies() {
if (JS8MessageBox::Yes ==
JS8MessageBox::query_message(
this, tr("Reset Working Frequencies"),
tr("Are you sure you want to discard your current "
"working frequencies and replace them with default "
"ones?"))) {
next_frequencies_.reset_to_defaults();
}
}
void Configuration::impl::insert_frequency() {
if (QDialog::Accepted == frequency_dialog_->exec()) {
ui_->frequencies_table_view->setCurrentIndex(
next_frequencies_.add(frequency_dialog_->item()));
ui_->frequencies_table_view->resizeColumnToContents(
FrequencyList_v3::mode_column);
}
}
void Configuration::impl::hop_to_station() {
/*
* If there is a valid selection in the station table, create a station
* instance from the row data and emit the manual_band_hop_requested signal
*/
auto selection_model = ui_->stations_table_view->selectionModel();
if (!selection_model || !selection_model->hasSelection()) {
return;
}
auto selected_rows = selection_model->selectedRows();
if (selected_rows.isEmpty()) {
return;
}
QModelIndex index = selected_rows.back();
// Get the time strings
QString switch_at_str =
index.sibling(index.row(), StationList::Column::switch_at_column)
.data(Qt::EditRole)
.toString();
QString switch_until_str =
index.sibling(index.row(), StationList::Column::switch_until_column)
.data(Qt::EditRole)
.toString();
// Create QDateTime objects with today's date and the times from the model
QTime at_time = QTime::fromString(switch_at_str, "hh:mm");
QTime until_time = QTime::fromString(switch_until_str, "hh:mm");
StationList::Station station{
index.sibling(index.row(), StationList::Column::band_column)
.data(Qt::EditRole)
.toString(),
index.sibling(index.row(), StationList::Column::frequency_column)
.data(Qt::EditRole)
.value<Radio::Frequency>(),
QDateTime(QDate(2000, 1, 1), at_time, QTimeZone::utc()),
QDateTime(QDate(2000, 1, 1), until_time, QTimeZone::utc()),
index.sibling(index.row(), StationList::Column::description_column)
.data(Qt::EditRole)
.toString()};
Q_EMIT self_->manual_band_hop_requested(station);
}
void Configuration::impl::delete_stations() {
auto selection_model = ui_->stations_table_view->selectionModel();
selection_model->select(selection_model->selection(),
QItemSelectionModel::SelectCurrent |
QItemSelectionModel::Rows);
next_stations_.removeDisjointRows(selection_model->selectedRows());
ui_->stations_table_view->resizeColumnToContents(StationList::band_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::frequency_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_at_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_until_column);
}
void Configuration::impl::insert_station() {
if (QDialog::Accepted == station_dialog_->exec()) {
auto station = station_dialog_->station();
if (station.frequency_ == 0) {
Frequency l;
Frequency h;
if (bands_.findFreq(station.band_name_, &l, &h)) {
station.frequency_ = l;
}
}
ui_->stations_table_view->setCurrentIndex(next_stations_.add(station));
ui_->stations_table_view->resizeColumnToContents(
StationList::band_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::frequency_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_at_column);
ui_->stations_table_view->resizeColumnToContents(
StationList::switch_until_column);
}
}
void Configuration::impl::on_save_path_select_push_button_clicked(
bool /* checked */) {
QFileDialog fd{this, tr("Save Directory"),
ui_->save_path_display_label->text()};
fd.setFileMode(QFileDialog::Directory);
fd.setOption(QFileDialog::ShowDirsOnly);
if (fd.exec()) {
if (fd.selectedFiles().size()) {
ui_->save_path_display_label->setText(fd.selectedFiles().at(0));
}
}
}
void Configuration::impl::on_calibration_intercept_spin_box_valueChanged(
double) {
rig_active_ = false; // force reset
}
void Configuration::impl::on_calibration_slope_ppm_spin_box_valueChanged(
double) {
rig_active_ = false; // force reset
}
bool Configuration::impl::have_rig() {
if (!open_rig()) {
JS8MessageBox::critical_message(this, tr("Rig control error"),
tr("Failed to open connection to rig"));
}
return rig_active_;
}
bool Configuration::impl::open_rig(bool force) {
auto result = false;
auto const rig_data = gather_rig_data();
if (force || !rig_active_ || rig_data != saved_rig_params_) {
try {
close_rig();
// create a new Transceiver object
auto rig =
transceiver_factory_.create(rig_data, transceiver_thread_);
cached_rig_state_ = Transceiver::TransceiverState{};
// hook up Configuration transceiver control signals to Transceiver
// slots
//
// these connections cross the thread boundary
rig_connections_
<< connect(this, &Configuration::impl::set_transceiver,
rig.get(), &Transceiver::set);
// hook up Transceiver signals to Configuration signals
//
// these connections cross the thread boundary
rig_connections_ << connect(
rig.get(), &Transceiver::resolution, this,
[this](int resolution) { rig_resolution_ = resolution; });
rig_connections_
<< connect(rig.get(), &Transceiver::update, this,
&Configuration::impl::handle_transceiver_update);
rig_connections_
<< connect(rig.get(), &Transceiver::failure, this,
&Configuration::impl::handle_transceiver_failure);
// setup thread safe startup and close down semantics
rig_connections_
<< connect(this, &Configuration::impl::start_transceiver,
rig.get(), &Transceiver::start);
rig_connections_
<< connect(this, &Configuration::impl::stop_transceiver,
rig.get(), &Transceiver::stop);
auto p = rig.release(); // take ownership
// schedule destruction on thread quit
connect(transceiver_thread_, &QThread::finished, p,
&QObject::deleteLater);
// schedule eventual destruction for non-closing situations
//
// must be queued connection to avoid premature
// self-immolation since finished signal is going to be
// emitted from the object that will get destroyed in its
// own stop slot i.e. a same thread signal to slot
// connection which by default will be reduced to a method
// function call.
connect(p, &Transceiver::finished, p, &Transceiver::deleteLater,
Qt::QueuedConnection);
ui_->test_CAT_push_button->setStyleSheet({});
rig_active_ = true;
Q_EMIT start_transceiver(
++transceiver_command_number_); // start rig on its thread
result = true;
} catch (std::exception const &e) {
handle_transceiver_failure(e.what());
}
saved_rig_params_ = rig_data;
rig_changed_ = true;
} else {
result = true;
}
return result;
}
void Configuration::impl::set_cached_mode() {
MODE mode{Transceiver::UNK};
// override cache mode with what we want to enforce which includes
// UNK (unknown) where we want to leave the rig mode untouched
switch (data_mode_) {
case data_mode_USB:
mode = Transceiver::USB;
break;
case data_mode_data:
mode = Transceiver::DIG_U;
break;
default:
break;
}
cached_rig_state_.mode(mode);
}
void Configuration::impl::transceiver_frequency(Frequency f) {
cached_rig_state_.online(true); // we want the rig online
set_cached_mode();
// apply any offset & calibration
// we store the offset here for use in feedback from the rig, we
// cannot absolutely determine if the offset should apply but by
// simply picking an offset when the Rx frequency is set and
// sticking to it we get sane behaviour
cached_rig_state_.frequency(apply_calibration(f));
Q_EMIT set_transceiver(cached_rig_state_, ++transceiver_command_number_);
}
void Configuration::impl::transceiver_tx_frequency(Frequency f) {
Q_ASSERT(!f || split_mode());
if (split_mode()) {
cached_rig_state_.online(true); // we want the rig online
set_cached_mode();
cached_rig_state_.split(f);
cached_rig_state_.tx_frequency(f);
// lookup offset for tx and apply calibration
if (f) {
// apply and offset and calibration
// we store the offset here for use in feedback from the
// rig, we cannot absolutely determine if the offset should
// apply but by simply picking an offset when the Rx
// frequency is set and sticking to it we get sane behaviour
cached_rig_state_.tx_frequency(apply_calibration(f));
}
Q_EMIT set_transceiver(cached_rig_state_,
++transceiver_command_number_);
}
}
void Configuration::impl::transceiver_mode(MODE m) {
cached_rig_state_.online(true); // we want the rig online
cached_rig_state_.mode(m);
Q_EMIT set_transceiver(cached_rig_state_, ++transceiver_command_number_);
}
void Configuration::impl::transceiver_ptt(bool on) {
cached_rig_state_.online(true); // we want the rig online
set_cached_mode();
cached_rig_state_.ptt(on);
Q_EMIT set_transceiver(cached_rig_state_, ++transceiver_command_number_);
}
void Configuration::impl::sync_transceiver(bool /*force_signal*/) {
// pass this on as cache must be ignored
// Q_EMIT sync (force_signal);
}
void Configuration::impl::handle_transceiver_update(
TransceiverState const &state, unsigned sequence_number) {
qCDebug(configuration_js8)
<< "Configuration::handle_transceiver_update: Transceiver State #:"
<< sequence_number << state;
// only follow rig on some information, ignore other stuff
cached_rig_state_.online(state.online());
cached_rig_state_.frequency(state.frequency());
cached_rig_state_.mode(state.mode());
cached_rig_state_.split(state.split());
if (state.online()) {
ui_->test_PTT_push_button->setChecked(state.ptt());
if (isVisible()) {
ui_->test_CAT_push_button->setStyleSheet(
"QPushButton {background-color: green;}");
auto const &rig = ui_->rig_combo_box->currentText();
auto ptt_method = static_cast<TransceiverFactory::PTTMethod>(
ui_->PTT_method_button_group->checkedId());
auto CAT_PTT_enabled = transceiver_factory_.has_CAT_PTT(rig);
ui_->test_PTT_push_button->setEnabled(
(TransceiverFactory::PTT_method_CAT == ptt_method &&
CAT_PTT_enabled) ||
TransceiverFactory::PTT_method_DTR == ptt_method ||
TransceiverFactory::PTT_method_RTS == ptt_method);
}
} else {
close_rig();
}
// pass on to clients if current command is processed
if (sequence_number == transceiver_command_number_) {
TransceiverState reported_state{state};
// take off calibration & offset
reported_state.frequency(
remove_calibration(reported_state.frequency()));
if (reported_state.tx_frequency()) {
// take off calibration & offset
reported_state.tx_frequency(
remove_calibration(reported_state.tx_frequency()));
}
Q_EMIT self_->transceiver_update(reported_state);
}
}
void Configuration::impl::handle_transceiver_failure(QString const &reason) {
qCDebug(configuration_js8)
<< "Configuration::handle_transceiver_failure: reason:" << reason;
close_rig();
ui_->test_PTT_push_button->setChecked(false);
if (isVisible()) {
JS8MessageBox::critical_message(this, tr("Rig failure"), reason);
} else {
// pass on if our dialog isn't active
Q_EMIT self_->transceiver_failure(reason);
}
}
void Configuration::impl::close_rig() {
ui_->test_PTT_push_button->setEnabled(false);
// revert to no rig configured
if (rig_active_) {
ui_->test_CAT_push_button->setStyleSheet(
"QPushButton {background-color: red;}");
Q_EMIT stop_transceiver();
for (auto const &connection : rig_connections_) {
disconnect(connection);
}
rig_connections_.clear();
rig_active_ = false;
}
}
// find the audio device that matches the specified name, also
// populate into the selection combo box with any devices we find in
// the search
QAudioDevice Configuration::impl::find_audio_device(QAudioDevice::Mode const mode,
QComboBox *const combo_box,
QString const &saved_id,
QString const &saved_name,
bool const allow_monitors)
{
Q_EMIT self_->enumerating_audio_devices();
auto const &devices = (mode == QAudioDevice::Input)
? QMediaDevices::audioInputs()
: QMediaDevices::audioOutputs();
// ---- 1) Exact ID match (preferred, stable, no collisions)
if (!saved_id.isEmpty()) {
for (auto const &p : devices) {
if (p.id() == saved_id) {
combo_box->insertItem(0, p.description(), QVariant::fromValue(p));
combo_box->setCurrentIndex(0);
return p;
}
}
}
// ---- 2) Legacy: match by description, but choose deterministically
if (!saved_name.isEmpty()) {
QList<QAudioDevice> matches;
matches.reserve(devices.size());
for (auto const &p : devices) {
if (p.description() != saved_name)
continue;
#ifdef Q_OS_LINUX
if (!allow_monitors && is_monitor(p))
continue;
#endif
matches.push_back(p);
}
auto best = QAudioDevice{};
int bestScore = std::numeric_limits<int>::min();
for (auto const &p : matches) {
int score = 0;
#ifdef Q_OS_LINUX
if (is_monitor(p)) score -= 1000; // only reachable if allow_monitors==true
if (looks_like_capture(p)) score += 200; // for Input restores
#endif
// Soft hint: don't gate on mode, just prefer it when truthful
if (p.mode() == mode) score += 10;
else score -= 10;
// Prefer stable-looking IDs over empty/odd ones
if (!p.id().isEmpty()) score += 1;
if (score > bestScore) {
bestScore = score;
best = p;
}
}
if (!best.isNull()) {
combo_box->insertItem(0, best.description(), QVariant::fromValue(best));
combo_box->setCurrentIndex(0);
return best;
}
// Not found (or filtered out as monitor)
combo_box->insertItem(0,
saved_name + " (" + tr("Not found", "audio device missing") + ")",
QVariant::fromValue(QAudioDevice{}));
combo_box->setCurrentIndex(0);
}
return {};
}
// load the available audio devices into the selection combo box and
// select the default device if the current device isn't set or isn't
// available
void Configuration::impl::load_audio_devices(QAudioDevice::Mode const mode,
QComboBox *combo_box,
QAudioDevice *device,
bool const show_monitors)
{
combo_box->clear();
Q_EMIT self_->enumerating_audio_devices();
int current_index = -1;
auto const &devices = (mode == QAudioDevice::Input)
? QMediaDevices::audioInputs()
: QMediaDevices::audioOutputs();
// Count duplicate descriptions
QHash<QString, int> descCounts;
descCounts.reserve(devices.size());
for (auto const &p : devices) {
#ifdef Q_OS_LINUX
if (mode == QAudioDevice::Input && !show_monitors && is_monitor(p))
continue;
#endif
descCounts[p.description()] += 1;
}
auto const wanted_id = (device && !device->isNull()) ? device->id() : QString{};
for (auto const &p : devices) {
#ifdef Q_OS_LINUX
if (mode == QAudioDevice::Input && !show_monitors && is_monitor(p))
continue;
#endif
qCDebug(configuration_js8)
<< "Configuration::impl::load_audio_devices" << Qt::endl
<< " id:" << p.id() << Qt::endl
<< " name:" << p.description() << Qt::endl
<< " mode:" << p.mode() << Qt::endl
<< " channelConfiguration:" << p.channelConfiguration()
<< Qt::endl
<< " supportedSampleFormats:" << p.supportedSampleFormats()
<< Qt::endl
<< " preferredFormat:" << p.preferredFormat();
auto const formats = p.supportedSampleFormats();
// Filter out devices that do not support PCM sample formats.
if (!std::any_of(formats.begin(), formats.end(), [](auto const format) {
return (format == QAudioFormat::SampleFormat::Int16 ||
format == QAudioFormat::SampleFormat::Int32 ||
format == QAudioFormat::SampleFormat::Float);
})) {
continue;
}
auto const base = p.description();
auto const label = make_label(base, p, descCounts.value(base, 1));
combo_box->addItem(label, QVariant::fromValue(p));
if (!wanted_id.isEmpty() && p.id() == wanted_id) {
current_index = combo_box->count() - 1;
}
}
// Make the UX smoother by picking a fallback
if (current_index < 0 && combo_box->count() > 0) {
current_index = 0;
}
combo_box->setCurrentIndex(current_index);
}
// Enable only the channels that are supported by the selected audio device.
// The function above will have provided the device channel configuration,
// which is a QAudioFormat::ChannelConfig enumeration, as the item data of
// the source combo box.
//
// - If the channel configuration isn't unknown, then the device is usable,
// perhaps only with a monaural channel, but at least usable.
//
// - If the device is usable, and has a configuration something other than
// a monaural channel, then we're at least looking at a stereo device.
// The device might have many more channels that just a left and right,
// but it's at least stereo.
void Configuration::impl::update_audio_channels(
QComboBox const *const source_combo_box,
QComboBox const *combo_box,
int const index,
bool const allow_both)
{
auto dev = source_combo_box->itemData(index).value<QAudioDevice>();
int ch = 0;
if (!dev.isNull())
ch = dev.preferredFormat().channelCount();
const auto cfg = dev.channelConfiguration();
bool usable = false;
bool mono = false;
bool stereo = false;
if (ch > 0) {
usable = true;
mono = (ch == 1);
stereo = (ch >= 2);
} else {
// fallback for platforms/backends where preferredFormat isn't informative
usable = (cfg != QAudioFormat::ChannelConfigUnknown);
mono = usable && (cfg == QAudioFormat::ChannelConfigMono);
stereo = usable && (cfg != QAudioFormat::ChannelConfigMono);
}
auto model = qobject_cast<QStandardItemModel *>(combo_box->model());
if (!model) return;
// If unknown, leaving Mono enabled avoids "everything disabled" UX.
model->item(AudioDevice::Mono)->setEnabled(mono || !usable);
model->item(AudioDevice::Left)->setEnabled(stereo);
model->item(AudioDevice::Right)->setEnabled(stereo);
model->item(AudioDevice::Both)->setEnabled(stereo && allow_both);
}
void Configuration::impl::find_tab(QWidget *target) {
for (auto const *parent = target->parentWidget(); parent;
parent = parent->parentWidget()) {
if (auto const index = ui_->configuration_tabs->indexOf(parent);
index != -1) {
ui_->configuration_tabs->setCurrentIndex(index);
break;
}
}
target->setFocus();
}
// load all the supported rig names into the selection combo box
void Configuration::impl::enumerate_rigs() {
ui_->rig_combo_box->clear();
auto rigs = transceiver_factory_.supported_transceivers();
for (auto r = rigs.cbegin(); r != rigs.cend(); ++r) {
if ("None" == r.key()) {
// put None first
ui_->rig_combo_box->insertItem(0, r.key(), r.value().model_number_);
} else {
int i;
for (i = 1; i < ui_->rig_combo_box->count() &&
(r.key().toLower() >
ui_->rig_combo_box->itemText(i).toLower());
++i)
;
if (i < ui_->rig_combo_box->count())
ui_->rig_combo_box->insertItem(i, r.key(),
r.value().model_number_);
else
ui_->rig_combo_box->addItem(r.key(), r.value().model_number_);
}
}
ui_->rig_combo_box->setCurrentText(rig_params_.rig_name);
}
void Configuration::impl::fill_port_combo_box(QComboBox *cb) {
auto current_text = cb->currentText();
cb->clear();
Q_FOREACH (auto const &p, QSerialPortInfo::availablePorts()) {
if (!p.portName().contains("NULL")) // virtual serial port pairs
{
// remove possibly confusing Windows device path (OK because
// it gets added back by Hamlib)
cb->addItem(
p.systemLocation().remove(QRegularExpression{R"(^\\\\\.\\)"}));
}
}
cb->addItem("USB");
cb->setEditText(current_text);
}
auto Configuration::impl::apply_calibration(Frequency f) const -> Frequency {
if (frequency_calibration_disabled_)
return f;
return std::llround(calibration_.intercept +
(1. + calibration_.slope_ppm / 1.e6) * f);
}
auto Configuration::impl::remove_calibration(Frequency f) const -> Frequency {
if (frequency_calibration_disabled_)
return f;
return std::llround((f - calibration_.intercept) /
(1. + calibration_.slope_ppm / 1.e6));
}
// load the available network interfaces into the selection combo box
void Configuration::impl::load_network_interfaces(
CheckableItemComboBox *combo_box, QStringList current) {
combo_box->clear();
for (auto const &net_if : QNetworkInterface::allInterfaces()) {
auto flags = QNetworkInterface::IsUp | QNetworkInterface::CanMulticast;
if ((net_if.flags() & flags) == flags) {
bool check_it = current.contains(net_if.name());
if (net_if.flags() & QNetworkInterface::IsLoopBack) {
wsjtx_loopback_interface_name_ = net_if.name();
if (!current.size()) {
check_it = true;
}
}
auto item = combo_box->addCheckItem(
net_if.humanReadableName(), net_if.name(),
check_it ? Qt::Checked : Qt::Unchecked);
auto tip =
QString{"name(index): %1(%2) - %3"}
.arg(net_if.name())
.arg(net_if.index())
.arg(net_if.flags() & QNetworkInterface::IsUp ? "Up"
: "Down");
auto hw_addr = net_if.hardwareAddress();
if (hw_addr.size()) {
tip += QString{"\nhw: %1"}.arg(net_if.hardwareAddress());
}
auto aes = net_if.addressEntries();
if (aes.size()) {
tip += "\naddresses:";
for (auto const &ae : aes) {
tip += QString{"\n ip: %1/%2"}
.arg(ae.ip().toString())
.arg(ae.prefixLength());
}
}
item->setToolTip(tip);
}
}
}
// Attaches a trailing warning icon that shows when `isInvalid()` returns true.
// Returns the QAction* in case you want to tweak tooltip/icon later.
QAction *Configuration::impl::attachRequiredIndicator(QLineEdit *edit,
const QString &toolTip) {
Q_ASSERT(edit);
QAction *warn = edit->addAction(
edit->style()->standardIcon(QStyle::SP_MessageBoxWarning),
QLineEdit::TrailingPosition);
warn->setToolTip(toolTip);
warn->setVisible(false);
auto update = [edit, warn]() {
const bool invalid = edit->text().trimmed().isEmpty();
warn->setVisible(invalid);
};
connect(edit, &QLineEdit::textChanged, edit,
[update](const QString &) { update(); });
connect(edit, &QLineEdit::editingFinished, edit, update);
update();
return warn;
}
// get the selected network interfaces from the selection combo box
QStringList Configuration::impl::get_selected_network_interfaces(
CheckableItemComboBox *combo_box) {
QStringList interfaces;
auto model = static_cast<QStandardItemModel *>(combo_box->model());
for (int row = 0; row < model->rowCount(); ++row) {
if (Qt::Checked == model->item(row)->checkState()) {
interfaces << model->item(row)->data().toString();
}
}
return interfaces;
}
inline QString Configuration::impl::dev_id(QAudioDevice const& d) {
return d.isNull() ? QString{} : d.id();
}
inline bool Configuration::impl::is_monitor(QAudioDevice const& d) {
#ifdef Q_OS_LINUX
return !d.isNull() && d.id().endsWith(".monitor");
#else
Q_UNUSED(d);
return false;
#endif
}
inline bool Configuration::impl::looks_like_capture(QAudioDevice const& d) {
#ifdef Q_OS_LINUX
if (d.isNull()) return false;
const auto id = d.id();
return id.startsWith("alsa_input.") ||
id.startsWith("bluez_input.") ||
id.startsWith("pipewire_input.") ||
id.startsWith("jack_input.");
#else
Q_UNUSED(d);
return false;
#endif
}
// tail after last '.' is usually short: analog-stereo / iec958-stereo / monitor
inline QString Configuration::impl::id_tail(QAudioDevice const& d) {
if (d.isNull()) return {};
auto const parts = d.id().split('.');
return parts.isEmpty() ? d.id() : parts.last();
}
inline QString Configuration::impl::make_label(QString const& base, QAudioDevice const& d, int dupCount) {
if (dupCount <= 1)
return base;
#ifdef Q_OS_LINUX
if (is_monitor(d))
return base + " (Monitor)";
#endif
auto tail = id_tail(d);
if (!tail.isEmpty())
return base + " (" + tail + ")";
return base;
}
void Configuration::impl::run_diagnostics()
{
ui_->run_diagnostics_button->setEnabled(false);
ui_->copy_diagnostics_button->setEnabled(false);
QGuiApplication::setOverrideCursor(QCursor{Qt::WaitCursor});
const auto text = build_diagnostics_text();
ui_->diagnostics_text->setPlainText(text);
ui_->diagnostics_text->moveCursor(QTextCursor::Start);
QGuiApplication::restoreOverrideCursor();
ui_->run_diagnostics_button->setEnabled(true);
ui_->copy_diagnostics_button->setEnabled(true);
}
QString Configuration::impl::build_diagnostics_text() const
{
QString s;
QTextStream ts(&s);
ts << "JS8Call Diagnostics\n";
ts << "Timestamp: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n\n";
ts << "Application\n";
ts << " App name: " << QCoreApplication::applicationName() << "\n";
ts << " Org name: " << QCoreApplication::organizationName() << "\n";
ts << " Org domain: " << QCoreApplication::organizationDomain() << "\n";
ts << " Config file: " << settings_->fileName() << "\n";
ts << " Config dir: " << QFileInfo(settings_->fileName()).absolutePath() << "\n";
ts << " Data directory: " << writeable_data_dir_.absolutePath() << "\n\n";
ts << "System\n";
ts << " Qt: " << qVersion() << "\n";
ts << " OS: " << QSysInfo::prettyProductName() << "\n";
ts << " Kernel: " << QSysInfo::kernelType() << " " << QSysInfo::kernelVersion() << "\n";
ts << " CPU arch: " << QSysInfo::currentCpuArchitecture() << "\n";
ts << " AppImage: " << qEnvironmentVariable("APPIMAGE") << "\n\n";
ts << "Qt Plugin Paths\n";
ts << " PluginsPath: " << QLibraryInfo::path(QLibraryInfo::PluginsPath) << "\n";
ts << " LibraryPath: " << QLibraryInfo::path(QLibraryInfo::LibrariesPath) << "\n";
ts << " BinariesPath: " << QLibraryInfo::path(QLibraryInfo::BinariesPath) << "\n\n";
ts << "Qt Runtime Plugin Search Paths\n";
for (auto const& p : QCoreApplication::libraryPaths()) {
ts << " " << p << "\n";
}
ts << "\n";
ts << "Environment\n";
ts << " Qt / Cross-platform\n";
append_env(ts, "QT_PLUGIN_PATH");
append_env(ts, "QT_QPA_PLATFORM_PLUGIN_PATH");
append_env(ts, "QT_QPA_PLATFORM");
append_env(ts, "QT_DEBUG_PLUGINS");
append_env(ts, "QT_LOGGING_RULES");
append_env(ts, "QT_AUDIO_BACKEND");
ts << "\n";
#ifdef Q_OS_LINUX
ts << " Linux Audio\n";
append_env(ts, "PIPEWIRE_REMOTE");
append_env(ts, "PULSE_SERVER");
append_env(ts, "XDG_RUNTIME_DIR");
#endif
#ifdef Q_OS_WIN
ts << " Windows\n";
append_env(ts, "PATH");
#endif
ts << "\n";
auto const defaultIn = QMediaDevices::defaultAudioInput();
auto const defaultOut = QMediaDevices::defaultAudioOutput();
ts << "Qt Defaults\n";
ts << " Default input: "
<< (defaultIn.isNull() ? "<none>" : defaultIn.description() + " | " + defaultIn.id()) << "\n";
ts << " Default output: "
<< (defaultOut.isNull() ? "<none>" : defaultOut.description() + " | " + defaultOut.id()) << "\n\n";
auto selected = [](QComboBox* cb) -> QAudioDevice {
return cb ? cb->currentData().value<QAudioDevice>() : QAudioDevice{};
};
auto selIn = selected(ui_->sound_input_combo_box);
auto selOut = selected(ui_->sound_output_combo_box);
auto selNtf = selected(ui_->notification_sound_output_combo_box);
ts << "Selected (from dialog)\n";
ts << " Input: " << (selIn.isNull() ? "<none>" : (selIn.description() + " | " + selIn.id())) << "\n";
ts << " Output: " << (selOut.isNull() ? "<none>" : (selOut.description() + " | " + selOut.id())) << "\n";
ts << " Notification: " << (selNtf.isNull() ? "<none>" : (selNtf.description() + " | " + selNtf.id())) << "\n\n";
ts << dump_devices("QMediaDevices::audioInputs()", QMediaDevices::audioInputs());
ts << dump_devices("QMediaDevices::audioOutputs()", QMediaDevices::audioOutputs());
#if defined(Q_OS_LINUX)
ts << "System Audio (best effort)\n\n";
ts << "pactl list sources short:\n";
ts << run_cmd("pactl", {"list", "sources", "short"}, 1200) << "\n";
ts << "pactl list sinks short:\n";
ts << run_cmd("pactl", {"list", "sinks", "short"}, 1200) << "\n";
ts << "wpctl status:\n";
ts << run_cmd("wpctl", {"status"}, 1500) << "\n";
ts << "pactl info:\n";
ts << run_cmd("pactl", {"info"}, 1200) << "\n";
#elif defined(Q_OS_WIN)
ts << "Windows Audio (best effort)\n\n";
ts << "PowerShell Win32_SoundDevice:\n";
ts << run_cmd("powershell",
{"-NoProfile", "-Command",
"Get-CimInstance Win32_SoundDevice | Format-List Name,Manufacturer,Status,PNPDeviceID"},
2500) << "\n";
ts << "PowerShell AudioEndpoint:\n";
ts << run_cmd("powershell",
{"-NoProfile", "-Command",
"Get-PnpDevice -Class AudioEndpoint | Format-List FriendlyName,Status,InstanceId"},
2500) << "\n";
#endif
ts << "Hints\n";
ts << " - If your system shows a device but QMediaDevices does not, suspect QtMultimedia backend/plugin mismatch.\n";
#ifdef Q_OS_LINUX
ts << " - Try launching with QT_AUDIO_BACKEND=pulseaudio.\n";
#endif
ts << " - If QT_DEBUG_PLUGINS=1 is set before launch, plugin load failures will print to stderr.\n";
return s;
}
QString Configuration::impl::run_cmd(QString const& program,
QStringList const& args,
int timeout_ms = 1200)
{
QProcess p;
p.setProgram(program);
p.setArguments(args);
p.setProcessChannelMode(QProcess::MergedChannels);
p.start();
if (!p.waitForStarted(200))
return QString("[failed to start: %1]\n").arg(program);
if (!p.waitForFinished(timeout_ms)) {
p.kill();
p.waitForFinished(200);
return QString("[timeout after %1ms: %2]\n").arg(timeout_ms).arg(program);
}
const auto out = QString::fromLocal8Bit(p.readAll());
return out.isEmpty() ? QString("[no output: %1]\n").arg(program) : out;
}
void Configuration::impl::append_env(QTextStream& ts, char const* key)
{
const auto v = qEnvironmentVariable(key);
ts << " " << key << "=" << (v.isEmpty() ? "<unset>" : v) << "\n";
}
QString Configuration::impl::dump_devices(QString const& title,
QList<QAudioDevice> const& devices)
{
QString s;
QTextStream ts(&s);
ts << title << " (" << devices.size() << ")\n";
for (auto const& d : devices) {
ts << " - description: " << d.description() << "\n";
ts << " id: " << d.id() << "\n";
ts << " mode: " << int(d.mode()) << "\n";
ts << " chConfig: " << int(d.channelConfiguration()) << "\n";
ts << " prefFmt: "
<< d.preferredFormat().sampleRate() << " Hz, "
<< d.preferredFormat().channelCount() << " ch, "
<< int(d.preferredFormat().sampleFormat()) << "\n";
ts << " formats: ";
for (auto const f : d.supportedSampleFormats())
ts << int(f) << " ";
ts << "\n";
}
ts << "\n";
return s;
}
QString Configuration::test_notification_path(const QString &key) const {
if (!m_->ui_->notifications_check_box->isChecked()) {
return "";
}
// Walk the live table to find the path label for this key
for (int i = 0; i < m_->ui_->notifications_table_widget->rowCount(); i++) {
auto item = m_->ui_->notifications_table_widget->item(i, 0);
if (!item || item->data(Qt::UserRole).toString() != key) continue;
auto label = qobject_cast<QLabel *>(
m_->ui_->notifications_table_widget->cellWidget(i, 2));
if (label) return label->text();
}
return "";
}
#if !defined(QT_NO_DEBUG_STREAM)
ENUM_QDEBUG_OPS_IMPL(Configuration, DataMode);
#endif
ENUM_QDATASTREAM_OPS_IMPL(Configuration, DataMode);
ENUM_CONVERSION_OPS_IMPL(Configuration, DataMode);
Q_LOGGING_CATEGORY(configuration_js8, "configuration.js8", QtWarningMsg)