/** * @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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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:"), ®ion_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 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 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 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 rig_connections_; QScopedPointer 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 notifications_enabled_; QMap 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 Configuration::my_groups() const { return QSet(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 groups = my_groups(); groups.insert(group.trimmed()); m_->my_groups_ = groups.values(); m_->write_settings(); } void Configuration::removeGroup(QString const &group) { QSet 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 Configuration::auto_whitelist() const { return QSet(m_->auto_whitelist_.begin(), m_->auto_whitelist_.end()); } QSet Configuration::auto_blacklist() const { return QSet(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 Configuration::hb_blacklist() const { return QSet(m_->hb_blacklist_.begin(), m_->hb_blacklist_.end()); } QSet Configuration::spot_blacklist() const { return QSet(m_->spot_blacklist_.begin(), m_->spot_blacklist_.end()); } QSet Configuration::rx_callsign_blocklist() const { return QSet(m_->rx_callsign_blocklist_.begin(), m_->rx_callsign_blocklist_.end()); } QSet Configuration::primary_highlight_words() const { return QSet(m_->primary_highlight_words_.begin(), m_->primary_highlight_words_.end()); } QSet Configuration::secondary_highlight_words() const { return QSet(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 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{®ions_, &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::max()); ui_->n3fjp_server_port_spin_box->setMinimum(0); ui_->n3fjp_server_port_spin_box->setMaximum( std::numeric_limits::max()); ui_->n1mm_server_port_spin_box->setMinimum(0); ui_->n1mm_server_port_spin_box->setMaximum( std::numeric_limits::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(&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(&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(®ions_); // // 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{®ions_, 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> 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 VERSION "}) .toString(); hb_ = settings_->value("HBMessage", QString{"HB "}).toString(); cq_ = settings_->value("CQMessage", QString{"CQ CQ CQ "}).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(); if (settings_->contains(versionedFrequenciesSettingsKey)) { auto const &v = settings_->value(versionedFrequenciesSettingsKey); if (v.isValid()) { frequencies_.frequency_list( v.value()); } 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_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()); 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(); rig_params_.stop_bits = settings_ ->value("CATStopBits", QVariant::fromValue(TransceiverFactory::default_stop_bits)) .value(); rig_params_.handshake = settings_ ->value("CATHandshake", QVariant::fromValue(TransceiverFactory::handshake_default)) .value(); 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(); rig_params_.audio_source = settings_ ->value( "TXAudioSource", QVariant::fromValue(TransceiverFactory::TX_audio_source_front)) .value(); rig_params_.ptt_port = settings_->value("PTTport").toString(); data_mode_ = settings_->value("DataMode", QVariant::fromValue(data_mode_none)) .value(); 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(); 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(*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( 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( 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(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( 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(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( ui_->CAT_data_bits_button_group->checkedId()); result.stop_bits = static_cast( ui_->CAT_stop_bits_button_group->checkedId()); result.handshake = static_cast( 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( 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( ui_->TX_audio_source_button_group->checkedId()); result.split_mode = static_cast( 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(); 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(); 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(); 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( 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( 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(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("enabledCheckbox"); if (enabledCheckbox) { notifications_enabled_[key] = enabledCheckbox->isChecked(); } auto pathWidget = ui_->notifications_table_widget->cellWidget(i, 2); QLabel *pathLabel = qobject_cast(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 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(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(), 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( 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 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::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 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(); 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(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(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() ? "" : defaultIn.description() + " | " + defaultIn.id()) << "\n"; ts << " Default output: " << (defaultOut.isNull() ? "" : defaultOut.description() + " | " + defaultOut.id()) << "\n\n"; auto selected = [](QComboBox* cb) -> QAudioDevice { return cb ? cb->currentData().value() : 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() ? "" : (selIn.description() + " | " + selIn.id())) << "\n"; ts << " Output: " << (selOut.isNull() ? "" : (selOut.description() + " | " + selOut.id())) << "\n"; ts << " Notification: " << (selNtf.isNull() ? "" : (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() ? "" : v) << "\n"; } QString Configuration::impl::dump_devices(QString const& title, QList 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( 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)