#include "settings_dialog.hpp" #include "ui_settings_dialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace scwx { namespace qt { namespace ui { static const std::string logPrefix_ = "scwx::qt::ui::settings_dialog"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); struct ColorTableConversions { uint16_t rangeMin {0u}; uint16_t rangeMax {255u}; float offset {0.0f}; float scale {1.0f}; }; static const std::array, 15> kColorTableTypes_ {std::pair {"BR", "BR"}, std::pair {"BV", "BV"}, std::pair {"SW", "SW"}, std::pair {"ZDR", "ZDR"}, std::pair {"PHI2", "KDP (L2)"}, std::pair {"CC", "CC"}, std::pair {"DOD", "DOD"}, std::pair {"DSD", "DSD"}, std::pair {"ET", "ET"}, std::pair {"OHP", "OHP"}, std::pair {"PHI3", "KDP"}, std::pair {"SRV", "SRV"}, std::pair {"STP", "STP"}, std::pair {"VIL", "VIL"}, std::pair {"???", "Default"}}; // Color table conversions for display, roughly based upon: // - ICD for RDA-RPG: Data Moment Characteristics and Conversion for Data Names // - ICD for the RPG to Class 1 User static const std::unordered_map kColorTableConversions_ {{"BR", {0u, 255u, 66.0f, 2.0f}}, {"BV", {0u, 255u, 129.0f, 2.0f}}, {"SW", {0u, 255u, 129.0f, 2.0f}}, {"ZDR", {0u, 1058u, 418.0f, 32.0f}}, {"PHI2", {0u, 1023u, 2.0f, 2.8361f}}, {"CC", {0u, 255u, -60.5f, 300.0f}}, {"DOD", {0u, 255u, 128.0f, 1.5f}}, {"DSD", {0u, 255u, 128.0f, 1.5f}}, {"ET", {0u, 255u, 2.0f, 1.0f}}, {"OHP", {0u, 255u, 0.0f, 2.5f}}, {"PHI3", {0u, 255u, 43.0f, 20.0f}}, {"SRV", {0u, 255u, 128.0f, 2.0f}}, {"STP", {0u, 255u, 0.0f, 1.25f}}, {"VIL", {0u, 255u, 1.0f, 2.5f}}, {"???", {0u, 15u, 0.0f, 1.0f}}}; class SettingsDialogImpl { public: explicit SettingsDialogImpl(SettingsDialog* self) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, settings_ {std::initializer_list { &defaultRadarSite_, &fontSizes_, &gridWidth_, &gridHeight_, &mapboxApiKey_, &debugEnabled_}} { // Configure default alert phenomena colors auto& paletteSettings = manager::SettingsManager::palette_settings(); int index = 0; for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) { QColorDialog::setCustomColor( index++, QColor(QString::fromStdString( paletteSettings.alert_color(phenomenon, true).GetDefault()))); QColorDialog::setCustomColor( index++, QColor(QString::fromStdString( paletteSettings.alert_color(phenomenon, false).GetDefault()))); } } ~SettingsDialogImpl() = default; void ConnectSignals(); void SetupGeneralTab(); void SetupPalettesColorTablesTab(); void SetupPalettesAlertsTab(); void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); void UpdateRadarDialogLocation(const std::string& id); void ApplyChanges(); void DiscardChanges(); void ResetToDefault(); static QImage GenerateColorTableImage(std::shared_ptr colorTable, std::uint16_t min, std::uint16_t max, float offset, float scale); static void LoadColorTablePreview(const std::string& key, const std::string& value, QLabel* imageLabel); static std::string RadarSiteLabel(std::shared_ptr& radarSite); static void SetBackgroundColor(const std::string& value, QFrame* frame); SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; settings::SettingsInterface defaultRadarSite_ {}; settings::SettingsInterface> fontSizes_ {}; settings::SettingsInterface gridWidth_ {}; settings::SettingsInterface gridHeight_ {}; settings::SettingsInterface mapboxApiKey_ {}; settings::SettingsInterface debugEnabled_ {}; std::unordered_map> colorTables_ {}; std::unordered_map> activeAlertColors_ {}; std::unordered_map> inactiveAlertColors_ {}; std::vector settings_; }; SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), p {std::make_unique(this)}, ui(new Ui::SettingsDialog) { ui->setupUi(this); // General p->SetupGeneralTab(); // Palettes > Color Tables p->SetupPalettesColorTablesTab(); // Palettes > Alerts p->SetupPalettesAlertsTab(); p->ConnectSignals(); } SettingsDialog::~SettingsDialog() { delete ui; } void SettingsDialogImpl::ConnectSignals() { QObject::connect(self_->ui->listWidget, &QListWidget::currentRowChanged, self_->ui->stackedWidget, &QStackedWidget::setCurrentIndex); QObject::connect(self_->ui->radarSiteSelectButton, &QAbstractButton::clicked, self_, [this]() { radarSiteDialog_->show(); }); QObject::connect(radarSiteDialog_, &RadarSiteDialog::accepted, self_, [this]() { std::string id = radarSiteDialog_->radar_site(); std::shared_ptr radarSite = config::RadarSite::Get(id); if (radarSite != nullptr) { self_->ui->radarSiteComboBox->setCurrentText( QString::fromStdString(RadarSiteLabel(radarSite))); } }); // Update the Radar Site dialog "map" location with the currently selected // radar site auto& defaultRadarSite = *defaultRadarSite_.GetSettingsVariable(); defaultRadarSite.RegisterValueStagedCallback( [this](const std::string& newValue) { UpdateRadarDialogLocation(newValue); }); QObject::connect( self_->ui->buttonBox, &QDialogButtonBox::clicked, self_, [this](QAbstractButton* button) { QDialogButtonBox::ButtonRole role = self_->ui->buttonBox->buttonRole(button); switch (role) { case QDialogButtonBox::ButtonRole::AcceptRole: // OK case QDialogButtonBox::ButtonRole::ApplyRole: // Apply ApplyChanges(); break; case QDialogButtonBox::ButtonRole::DestructiveRole: // Discard case QDialogButtonBox::ButtonRole::RejectRole: // Cancel DiscardChanges(); break; case QDialogButtonBox::ButtonRole::ResetRole: // Restore Defaults ResetToDefault(); break; } }); } void SettingsDialogImpl::SetupGeneralTab() { auto radarSites = config::RadarSite::GetAll(); // Sort radar sites by ID std::sort(radarSites.begin(), radarSites.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { return a->id() < b->id(); }); // Add sorted radar sites for (std::shared_ptr& radarSite : radarSites) { QString text = QString::fromStdString(RadarSiteLabel(radarSite)); self_->ui->radarSiteComboBox->addItem(text); } settings::GeneralSettings& generalSettings = manager::SettingsManager::general_settings(); defaultRadarSite_.SetSettingsVariable(generalSettings.default_radar_site()); defaultRadarSite_.SetMapFromValueFunction( [](const std::string& id) -> std::string { // Get the radar site associated with the ID std::shared_ptr radarSite = config::RadarSite::Get(id); if (radarSite == nullptr) { // No radar site found, just return the ID return id; } // Add location details to the radar site return RadarSiteLabel(radarSite); }); defaultRadarSite_.SetMapToValueFunction( [](const std::string& text) -> std::string { // Find the position of location details size_t pos = text.rfind(" ("); if (pos == std::string::npos) { // No location details found, just return the text return text; } // Remove location details from the radar site return text.substr(0, pos); }); defaultRadarSite_.SetEditWidget(self_->ui->radarSiteComboBox); defaultRadarSite_.SetResetButton(self_->ui->resetRadarSiteButton); UpdateRadarDialogLocation(generalSettings.default_radar_site().GetValue()); fontSizes_.SetSettingsVariable(generalSettings.font_sizes()); fontSizes_.SetEditWidget(self_->ui->fontSizesLineEdit); fontSizes_.SetResetButton(self_->ui->resetFontSizesButton); gridWidth_.SetSettingsVariable(generalSettings.grid_width()); gridWidth_.SetEditWidget(self_->ui->gridWidthSpinBox); gridWidth_.SetResetButton(self_->ui->resetGridWidthButton); gridHeight_.SetSettingsVariable(generalSettings.grid_height()); gridHeight_.SetEditWidget(self_->ui->gridHeightSpinBox); gridHeight_.SetResetButton(self_->ui->resetGridHeightButton); mapboxApiKey_.SetSettingsVariable(generalSettings.mapbox_api_key()); mapboxApiKey_.SetEditWidget(self_->ui->mapboxApiKeyLineEdit); mapboxApiKey_.SetResetButton(self_->ui->resetMapboxApiKeyButton); debugEnabled_.SetSettingsVariable(generalSettings.debug_enabled()); debugEnabled_.SetEditWidget(self_->ui->debugEnabledCheckBox); } void SettingsDialogImpl::SetupPalettesColorTablesTab() { settings::PaletteSettings& paletteSettings = manager::SettingsManager::palette_settings(); // Palettes > Color Tables QGridLayout* colorTableLayout = reinterpret_cast(self_->ui->colorTableContents->layout()); int colorTableRow = 0; for (auto& colorTableType : kColorTableTypes_) { QLabel* imageLabel = new QLabel(self_); QLineEdit* lineEdit = new QLineEdit(self_); QToolButton* openFileButton = new QToolButton(self_); QToolButton* resetButton = new QToolButton(self_); openFileButton->setText(QObject::tr("...")); resetButton->setIcon( QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); resetButton->setVisible(false); imageLabel->setFrameShape(QFrame::Shape::Box); imageLabel->setFrameShadow(QFrame::Shadow::Plain); imageLabel->setVisible(false); colorTableLayout->addWidget( new QLabel(colorTableType.second.c_str(), self_), colorTableRow, 0); colorTableLayout->addWidget(imageLabel, colorTableRow, 1); colorTableLayout->addWidget(lineEdit, colorTableRow, 2); colorTableLayout->addWidget(openFileButton, colorTableRow, 3); colorTableLayout->addWidget(resetButton, colorTableRow, 4); ++colorTableRow; // Create settings interface auto result = colorTables_.emplace( colorTableType.first, settings::SettingsInterface {}); auto& pair = *result.first; auto& colorTable = pair.second; // Add to settings list settings_.push_back(&colorTable); auto& colorTableVariable = paletteSettings.palette(colorTableType.first); colorTable.SetSettingsVariable(colorTableVariable); colorTable.SetEditWidget(lineEdit); colorTable.SetResetButton(resetButton); colorTableVariable.RegisterValueStagedCallback( [colorTableType, imageLabel](const std::string& value) { LoadColorTablePreview(colorTableType.first, value, imageLabel); }); LoadColorTablePreview( colorTableType.first, colorTableVariable.GetValue(), imageLabel); QObject::connect( openFileButton, &QAbstractButton::clicked, self_, [this, lineEdit]() { static const std::string paletteFilter = "Color Palettes (*.pal)"; static const std::string allFilter = "All Files (*)"; QFileDialog* dialog = new QFileDialog(self_); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setNameFilters({QObject::tr(paletteFilter.c_str()), QObject::tr(allFilter.c_str())}); dialog->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(dialog, &QFileDialog::fileSelected, self_, [this, lineEdit](const QString& file) { QString path = QDir::toNativeSeparators(file); logger_->info("Selected palette: {}", path.toStdString()); lineEdit->setText(path); // setText does not emit the textEdited signal emit lineEdit->textEdited(path); }); dialog->open(); }); } } void SettingsDialogImpl::SetupPalettesAlertsTab() { settings::PaletteSettings& paletteSettings = manager::SettingsManager::palette_settings(); // Palettes > Alerts QGridLayout* alertsLayout = reinterpret_cast(self_->ui->alertsFrame->layout()); QLabel* phenomenonLabel = new QLabel(QObject::tr("Phenomenon"), self_); QLabel* activeLabel = new QLabel(QObject::tr("Active"), self_); QLabel* inactiveLabel = new QLabel(QObject::tr("Inactive"), self_); QFont boldFont; boldFont.setBold(true); phenomenonLabel->setFont(boldFont); activeLabel->setFont(boldFont); inactiveLabel->setFont(boldFont); alertsLayout->addWidget(phenomenonLabel, 0, 0); alertsLayout->addWidget(activeLabel, 0, 1, 1, 4); alertsLayout->addWidget(inactiveLabel, 0, 5, 1, 4); int alertsRow = 1; for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) { QFrame* activeFrame = new QFrame(self_); QFrame* inactiveFrame = new QFrame(self_); QLineEdit* activeEdit = new QLineEdit(self_); QLineEdit* inactiveEdit = new QLineEdit(self_); QToolButton* activeButton = new QToolButton(self_); QToolButton* inactiveButton = new QToolButton(self_); QToolButton* activeResetButton = new QToolButton(self_); QToolButton* inactiveResetButton = new QToolButton(self_); activeFrame->setMinimumHeight(24); activeFrame->setMinimumWidth(24); activeFrame->setFrameShape(QFrame::Shape::Box); activeFrame->setFrameShadow(QFrame::Shadow::Plain); inactiveFrame->setMinimumHeight(24); inactiveFrame->setMinimumWidth(24); inactiveFrame->setFrameShape(QFrame::Shape::Box); inactiveFrame->setFrameShadow(QFrame::Shadow::Plain); activeButton->setIcon( QIcon {":/res/icons/font-awesome-6/palette-solid.svg"}); inactiveButton->setIcon( QIcon {":/res/icons/font-awesome-6/palette-solid.svg"}); activeResetButton->setIcon( QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); inactiveResetButton->setIcon( QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); alertsLayout->addWidget( new QLabel(QObject::tr(awips::GetPhenomenonText(phenomenon).c_str()), self_), alertsRow, 0); alertsLayout->addWidget(activeFrame, alertsRow, 1); alertsLayout->addWidget(activeEdit, alertsRow, 2); alertsLayout->addWidget(activeButton, alertsRow, 3); alertsLayout->addWidget(activeResetButton, alertsRow, 4); alertsLayout->addWidget(inactiveFrame, alertsRow, 5); alertsLayout->addWidget(inactiveEdit, alertsRow, 6); alertsLayout->addWidget(inactiveButton, alertsRow, 7); alertsLayout->addWidget(inactiveResetButton, alertsRow, 8); ++alertsRow; // Create settings interface auto activeResult = activeAlertColors_.emplace( phenomenon, settings::SettingsInterface {}); auto inactiveResult = inactiveAlertColors_.emplace( phenomenon, settings::SettingsInterface {}); auto& activeColor = activeResult.first->second; auto& inactiveColor = inactiveResult.first->second; // Add to settings list settings_.push_back(&activeColor); settings_.push_back(&inactiveColor); auto& activeSetting = paletteSettings.alert_color(phenomenon, true); auto& inactiveSetting = paletteSettings.alert_color(phenomenon, false); activeColor.SetSettingsVariable(activeSetting); activeColor.SetEditWidget(activeEdit); activeColor.SetResetButton(activeResetButton); inactiveColor.SetSettingsVariable(inactiveSetting); inactiveColor.SetEditWidget(inactiveEdit); inactiveColor.SetResetButton(inactiveResetButton); SetBackgroundColor(activeSetting.GetValue(), activeFrame); SetBackgroundColor(inactiveSetting.GetValue(), inactiveFrame); activeSetting.RegisterValueStagedCallback( [activeFrame](const std::string& value) { SetBackgroundColor(value, activeFrame); }); inactiveSetting.RegisterValueStagedCallback( [inactiveFrame](const std::string& value) { SetBackgroundColor(value, inactiveFrame); }); QObject::connect(activeButton, &QAbstractButton::clicked, self_, [=]() { ShowColorDialog(activeEdit, activeFrame); }); QObject::connect(inactiveButton, &QAbstractButton::clicked, self_, [=]() { ShowColorDialog(inactiveEdit, inactiveFrame); }); } } QImage SettingsDialogImpl::GenerateColorTableImage( std::shared_ptr colorTable, std::uint16_t min, std::uint16_t max, float offset, float scale) { std::size_t width = max - min + 1u; std::size_t height = 1u; QImage image(static_cast(width), static_cast(height), QImage::Format::Format_ARGB32); for (std::size_t i = min; i <= max; ++i) { const float value = (i - offset) / scale; boost::gil::rgba8_pixel_t pixel = colorTable->Color(value); image.setPixel(static_cast(i - min), 0, qRgba(static_cast(pixel[0]), static_cast(pixel[1]), static_cast(pixel[2]), static_cast(pixel[3]))); } return image; } void SettingsDialogImpl::LoadColorTablePreview(const std::string& key, const std::string& value, QLabel* imageLabel) { scwx::util::async( [key, value, imageLabel]() { std::unique_ptr is = util::OpenFile(value); std::shared_ptr colorTable = common::ColorTable::Load(*is); if (colorTable->IsValid()) { auto& conversions = kColorTableConversions_.at(key); QPixmap image = QPixmap::fromImage(GenerateColorTableImage(colorTable, conversions.rangeMin, conversions.rangeMax, conversions.offset, conversions.scale)) .scaled(64, 20); imageLabel->setPixmap(image); QMetaObject::invokeMethod( imageLabel, [imageLabel] { imageLabel->setVisible(true); }); } else { imageLabel->clear(); QMetaObject::invokeMethod( imageLabel, [imageLabel] { imageLabel->setVisible(false); }); } }); } void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit, QFrame* frame) { QColorDialog* dialog = new QColorDialog(self_); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); QColor initialColor(lineEdit->text()); if (initialColor.isValid()) { dialog->setCurrentColor(initialColor); } QObject::connect( dialog, &QColorDialog::colorSelected, self_, [this, lineEdit, frame](const QColor& color) { QString colorName = color.name(QColor::NameFormat::HexArgb); logger_->info("Selected color: {}", colorName.toStdString()); lineEdit->setText(colorName); // setText does not emit the textEdited signal emit lineEdit->textEdited(colorName); }); dialog->open(); } void SettingsDialogImpl::SetBackgroundColor(const std::string& value, QFrame* frame) { frame->setStyleSheet( QString::fromStdString(std::format("background-color: {}", value))); } void SettingsDialogImpl::UpdateRadarDialogLocation(const std::string& id) { std::shared_ptr radarSite = config::RadarSite::Get(id); if (radarSite != nullptr) { radarSiteDialog_->HandleMapUpdate(radarSite->latitude(), radarSite->longitude()); } } void SettingsDialogImpl::ApplyChanges() { logger_->info("Applying settings changes"); bool committed = false; for (auto& setting : settings_) { committed |= setting->Commit(); } if (committed) { manager::SettingsManager::SaveSettings(); } } void SettingsDialogImpl::DiscardChanges() { logger_->info("Discarding settings changes"); for (auto& setting : settings_) { setting->Reset(); } } void SettingsDialogImpl::ResetToDefault() { logger_->info("Restoring settings to default"); for (auto& setting : settings_) { setting->StageDefault(); } } std::string SettingsDialogImpl::RadarSiteLabel( std::shared_ptr& radarSite) { return std::format("{} ({})", radarSite->id(), radarSite->location_name()); } } // namespace ui } // namespace qt } // namespace scwx