diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 90b9b56f..74864ab7 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -258,6 +258,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/api_key_edit_widget.hpp source/scwx/qt/ui/collapsible_group.hpp source/scwx/qt/ui/county_dialog.hpp + source/scwx/qt/ui/custom_layer_dialog.hpp source/scwx/qt/ui/download_dialog.hpp source/scwx/qt/ui/edit_line_dialog.hpp source/scwx/qt/ui/edit_marker_dialog.hpp @@ -290,6 +291,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/api_key_edit_widget.cpp source/scwx/qt/ui/collapsible_group.cpp source/scwx/qt/ui/county_dialog.cpp + source/scwx/qt/ui/custom_layer_dialog.cpp source/scwx/qt/ui/download_dialog.cpp source/scwx/qt/ui/edit_line_dialog.cpp source/scwx/qt/ui/edit_marker_dialog.cpp @@ -321,6 +323,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/animation_dock_widget.ui source/scwx/qt/ui/collapsible_group.ui source/scwx/qt/ui/county_dialog.ui + source/scwx/qt/ui/custom_layer_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index ad504cc0..5b25a91a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -316,7 +316,7 @@ MainWindow::MainWindow(QWidget* parent) : p->layerDialog_ = new ui::LayerDialog(this); // Settings Dialog - p->settingsDialog_ = new ui::SettingsDialog(this); + p->settingsDialog_ = new ui::SettingsDialog(p->settings_, this); // Map Settings p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index b4882345..ebea2b21 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -185,8 +185,6 @@ public: bool UpdateStoredMapParameters(); void CheckLevel3Availability(); - std::string FindMapSymbologyLayer(); - common::Level2Product GetLevel2ProductOrDefault(const std::string& productName) const; @@ -1146,43 +1144,6 @@ void MapWidget::DumpLayerList() const logger_->info("Layers: {}", p->map_->layerIds().join(", ").toStdString()); } -std::string MapWidgetImpl::FindMapSymbologyLayer() -{ - std::string before = "ferry"; - - for (const QString& qlayer : styleLayers_) - { - const std::string layer = qlayer.toStdString(); - - // Draw below layers defined in map style - auto it = std::find_if(currentStyle_->drawBelow_.cbegin(), - currentStyle_->drawBelow_.cend(), - [&layer](const std::string& styleLayer) -> bool - { - // Perform case-insensitive matching - RE2 re {"(?i)" + styleLayer}; - if (re.ok()) - { - return RE2::FullMatch(layer, re); - } - else - { - // Fall back to basic comparison if RE - // doesn't compile - return layer == styleLayer; - } - }); - - if (it != currentStyle_->drawBelow_.cend()) - { - before = layer; - break; - } - } - - return before; -} - void MapWidgetImpl::AddLayers() { if (styleLayers_.isEmpty()) @@ -1218,7 +1179,8 @@ void MapWidgetImpl::AddLayers() { // Subsequent layers are drawn underneath the map symbology layer case types::MapLayer::MapUnderlay: - before = FindMapSymbologyLayer(); + before = util::maplibre::FindMapSymbologyLayer( + styleLayers_, currentStyle_->drawBelow_); break; // Subsequent layers are drawn after all style-defined layers diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index c6e93c76..a445da6d 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -107,6 +107,10 @@ public: SCWX_SETTINGS_ENUM_VALIDATOR(scwx::util::ClockFormat, scwx::util::ClockFormatIterator(), scwx::util::GetClockFormatName)); + + customStyleUrl_.SetValidator( + [](const std::string& value) + { return value.find("key=") == std::string::npos; }); customStyleDrawLayer_.SetValidator([](const std::string& value) { return !value.empty(); }); defaultAlertAction_.SetValidator( diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index ab36ebd9..e2c280b2 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace scwx::qt::settings @@ -19,6 +20,9 @@ namespace scwx::qt::settings static const std::string logPrefix_ = "scwx::qt::settings::settings_interface"; +static const QString kValidStyleSheet_ = ""; +static const QString kInvalidStyleSheet_ = "border: 2px solid red;"; + template class SettingsInterface::Impl { @@ -40,6 +44,7 @@ public: void UpdateEditWidget(); void UpdateResetButton(); void UpdateUnitLabel(); + void UpdateValidityDisplay(); SettingsInterface* self_; @@ -59,6 +64,8 @@ public: bool unitEnabled_ {false}; bool trimmingEnabled_ {false}; + + std::optional invalidTooltip_; }; template @@ -172,14 +179,13 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->context_.get(), [this](const QKeySequence& sequence) { - std::string value { + const std::string value { sequence.toString().toStdString()}; // Attempt to stage the value p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - - // TODO: Display invalid status + p->UpdateValidityDisplay(); }); } } @@ -194,7 +200,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->context_.get(), [this](const QString& text) { - QString trimmedText = + const QString trimmedText = p->trimmingEnabled_ ? text.trimmed() : text; // Map to value if required @@ -207,8 +213,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) // Attempt to stage the value p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - - // TODO: Display invalid status + p->UpdateValidityDisplay(); }); } else if constexpr (std::is_same_v) @@ -221,8 +226,8 @@ void SettingsInterface::SetEditWidget(QWidget* widget) [this](const QString& text) { // Convert to a double - bool ok; - double value = text.toDouble(&ok); + bool ok = false; + const double value = text.toDouble(&ok); if (ok) { // Attempt to stage the value @@ -235,6 +240,8 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->stagedValid_ = false; p->UpdateResetButton(); } + + p->UpdateValidityDisplay(); }); } else if constexpr (std::is_same_v>) @@ -279,8 +286,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) // Attempt to stage the value p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - - // TODO: Display invalid status + p->UpdateValidityDisplay(); }); } } @@ -364,6 +370,8 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + p->UpdateValidityDisplay(); }); } } @@ -415,6 +423,8 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + p->UpdateValidityDisplay(); }); } } @@ -500,6 +510,13 @@ void SettingsInterface::EnableTrimming(bool trimmingEnabled) p->trimmingEnabled_ = trimmingEnabled; } +template +void SettingsInterface::SetInvalidTooltip( + const std::optional& tooltip) +{ + p->invalidTooltip_ = std::move(tooltip); +} + template template void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) @@ -617,6 +634,15 @@ void SettingsInterface::Impl::UpdateUnitLabel() unitLabel_->setText(QString::fromStdString(unitAbbreviation_.value_or(""))); } +template +void SettingsInterface::Impl::UpdateValidityDisplay() +{ + editWidget_->setStyleSheet(stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + editWidget_->setToolTip( + invalidTooltip_ && !stagedValid_ ? invalidTooltip_->c_str() : ""); +} + template void SettingsInterface::Impl::UpdateResetButton() { diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 3d7d9d85..6bf586a7 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -130,6 +131,13 @@ public: */ void EnableTrimming(bool trimmingEnabled = true); + /** + * Set a tooltip to be displayed when an invalid input is given. + * + * @param tooltip the tooltip to be displayed + */ + void SetInvalidTooltip(const std::optional& tooltip); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp new file mode 100644 index 00000000..d7c963f6 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp @@ -0,0 +1,108 @@ +#include "custom_layer_dialog.hpp" +#include "ui_custom_layer_dialog.h" + +#include +#include +#include +#include + +#include + +namespace scwx::qt::ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::custom_layer_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class CustomLayerDialogImpl +{ +public: + explicit CustomLayerDialogImpl(CustomLayerDialog* self, + QMapLibre::Settings settings) : + self_(self), settings_(std::move(settings)) + { + } + + ~CustomLayerDialogImpl() = default; + CustomLayerDialogImpl(const CustomLayerDialogImpl&) = delete; + CustomLayerDialogImpl(CustomLayerDialogImpl&&) = delete; + CustomLayerDialogImpl& operator=(const CustomLayerDialogImpl&) = delete; + CustomLayerDialogImpl& operator=(CustomLayerDialogImpl&&) = delete; + + void handle_mapChanged(QMapLibre::Map::MapChange change); + + CustomLayerDialog* self_; + + QMapLibre::Settings settings_; + std::shared_ptr map_; +}; + +void CustomLayerDialogImpl::handle_mapChanged(QMapLibre::Map::MapChange change) +{ + if (change == QMapLibre::Map::MapChange::MapChangeDidFinishLoadingStyle) + { + auto& generalSettings = settings::GeneralSettings::Instance(); + const std::string& customStyleDrawLayer = + generalSettings.custom_style_draw_layer().GetStagedOrValue(); + + const QStringList layerIds = map_->layerIds(); + self_->ui->layerListWidget->clear(); + self_->ui->layerListWidget->addItems(layerIds); + + const std::string symbologyLayer = util::maplibre::FindMapSymbologyLayer( + layerIds, {customStyleDrawLayer}); + + const auto& symbologyItems = self_->ui->layerListWidget->findItems( + symbologyLayer.c_str(), Qt::MatchExactly); + if (!symbologyItems.isEmpty()) + { + self_->ui->layerListWidget->setCurrentItem(symbologyItems.first()); + } + } +} + +CustomLayerDialog::CustomLayerDialog(const QMapLibre::Settings& settings, + QWidget* parent) : + QDialog(parent), + p {std::make_unique(this, settings)}, + ui(new Ui::CustomLayerDialog) +{ + ui->setupUi(this); + + auto& generalSettings = settings::GeneralSettings::Instance(); + const auto& customStyleUrl = generalSettings.custom_style_url().GetValue(); + const auto mapProvider = + map::GetMapProvider(generalSettings.map_provider().GetValue()); + + // TODO render the map with a layer to show what they are selecting + p->map_ = std::make_shared( + nullptr, p->settings_, QSize(1, 1), devicePixelRatioF()); + + QString qUrl = QString::fromStdString(customStyleUrl); + + if (mapProvider == map::MapProvider::MapTiler) + { + qUrl.append("?key="); + qUrl.append(map::GetMapProviderApiKey(mapProvider)); + } + + p->map_->setStyleUrl(qUrl); + + QObject::connect(p->map_.get(), + &QMapLibre::Map::mapChanged, + this, + [this](QMapLibre::Map::MapChange change) + { p->handle_mapChanged(change); }); +} + +CustomLayerDialog::~CustomLayerDialog() +{ + delete ui; +} + +std::string CustomLayerDialog::selected_layer() +{ + return ui->layerListWidget->currentItem()->text().toStdString(); +} + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp new file mode 100644 index 00000000..ad0a2526 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace Ui +{ +class CustomLayerDialog; +} + +namespace scwx::qt::ui +{ + +class CustomLayerDialogImpl; + +class CustomLayerDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(CustomLayerDialog) + +public: + explicit CustomLayerDialog(const QMapLibre::Settings& settings, + QWidget* parent = nullptr); + ~CustomLayerDialog() override; + + std::string selected_layer(); + +private: + friend class CustomLayerDialogImpl; + std::unique_ptr p; + Ui::CustomLayerDialog* ui; +}; + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui new file mode 100644 index 00000000..2ce59321 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui @@ -0,0 +1,96 @@ + + + CustomLayerDialog + + + + 0 + 0 + 308 + 300 + + + + + 0 + 0 + + + + Custom Map Style Draw Layer + + + + + + + + + 0 + 0 + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + true + + + + + + + + + + 0 + 0 + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + CustomLayerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CustomLayerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 7ce55037..c1e6d501 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include #include #include +#include namespace scwx { @@ -106,7 +108,8 @@ static const std::unordered_map class SettingsDialogImpl { public: - explicit SettingsDialogImpl(SettingsDialog* self) : + explicit SettingsDialogImpl(SettingsDialog* self, + QMapLibre::Settings& mapSettings) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, alertAudioRadarSiteDialog_ {new RadarSiteDialog(self)}, @@ -114,6 +117,7 @@ public: countyDialog_ {new CountyDialog(self)}, wfoDialog_ {new WFODialog(self)}, fontDialog_ {new QFontDialog(self)}, + mapSettings_ {mapSettings}, fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { &defaultRadarSite_, @@ -219,6 +223,8 @@ public: WFODialog* wfoDialog_; QFontDialog* fontDialog_; + QMapLibre::Settings& mapSettings_; + QStandardItemModel* fontCategoryModel_; types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; @@ -292,9 +298,10 @@ public: std::vector settings_; }; -SettingsDialog::SettingsDialog(QWidget* parent) : +SettingsDialog::SettingsDialog(QMapLibre::Settings& mapSettings, + QWidget* parent) : QDialog(parent), - p {std::make_unique(this)}, + p {std::make_unique(this, mapSettings)}, ui(new Ui::SettingsDialog) { ui->setupUi(this); @@ -685,12 +692,39 @@ void SettingsDialogImpl::SetupGeneralTab() customStyleUrl_.SetSettingsVariable(generalSettings.custom_style_url()); customStyleUrl_.SetEditWidget(self_->ui->customMapUrlLineEdit); customStyleUrl_.SetResetButton(self_->ui->resetCustomMapUrlButton); + customStyleUrl_.SetInvalidTooltip( + "Remove anything following \"?key=\" in the URL"); customStyleUrl_.EnableTrimming(); customStyleDrawLayer_.SetSettingsVariable( generalSettings.custom_style_draw_layer()); customStyleDrawLayer_.SetEditWidget(self_->ui->customMapLayerLineEdit); customStyleDrawLayer_.SetResetButton(self_->ui->resetCustomMapLayerButton); + QObject::connect( + self_->ui->customMapLayerToolButton, + &QAbstractButton::clicked, + self_, + [this]() + { + // WA_DeleteOnClose manages memory + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto* customLayerDialog = new ui::CustomLayerDialog(mapSettings_); + customLayerDialog->setAttribute(Qt::WA_DeleteOnClose); + QObject::connect( + customLayerDialog, + &QDialog::accepted, + self_, + [this, customLayerDialog]() + { + auto newLayer = customLayerDialog->selected_layer(); + self_->ui->customMapLayerLineEdit->setText(newLayer.c_str()); + // setText does not emit the textEdited signal + Q_EMIT + self_->ui->customMapLayerLineEdit->textEdited(newLayer.c_str()); + }); + + customLayerDialog->open(); + }); defaultAlertAction_.SetSettingsVariable( generalSettings.default_alert_action()); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp index 82f00905..866e1ea8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Ui { @@ -24,7 +25,8 @@ private: Q_DISABLE_COPY(SettingsDialog) public: - explicit SettingsDialog(QWidget* parent = nullptr); + explicit SettingsDialog(QMapLibre::Settings& mapSettings, + QWidget* parent = nullptr); ~SettingsDialog(); private: diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index bb2389bc..f14c6a96 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -122,7 +122,7 @@ - 3 + 0 @@ -135,9 +135,9 @@ 0 - -303 + -260 511 - 733 + 812 @@ -608,6 +608,13 @@ + + + + ... + + + @@ -714,8 +721,8 @@ 0 0 - 98 - 28 + 80 + 18 diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 63f5112e..6a4c7d55 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -1,7 +1,9 @@ #include #include +#include #include +#include namespace scwx { @@ -120,6 +122,44 @@ void SetMapStyleUrl(const std::shared_ptr& mapContext, } } +std::string FindMapSymbologyLayer(const QStringList& styleLayers, + const std::vector& drawBelow) +{ + std::string before = "ferry"; + + for (const QString& qlayer : styleLayers) + { + const std::string layer = qlayer.toStdString(); + + // Draw below layers defined in map style + auto it = + std::ranges::find_if(drawBelow, + [&layer](const std::string& styleLayer) -> bool + { + // Perform case-insensitive matching + const RE2 re {"(?i)" + styleLayer}; + if (re.ok()) + { + return RE2::FullMatch(layer, re); + } + else + { + // Fall back to basic comparison if RE + // doesn't compile + return layer == styleLayer; + } + }); + + if (it != drawBelow.cend()) + { + before = layer; + break; + } + } + + return before; +} + } // namespace maplibre } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/maplibre.hpp b/scwx-qt/source/scwx/qt/util/maplibre.hpp index 7c2eb58b..c03be113 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.hpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.hpp @@ -37,6 +37,18 @@ glm::vec2 LatLongToScreenCoordinate(const QMapLibre::Coordinate& coordinate); void SetMapStyleUrl(const std::shared_ptr& mapContext, const std::string& url); +/** + * @brief Find the first layer which should be drawn above the radar products + * + * @param [in] styleLayers The layers of the style + * @param [in] drawBelow A list of RE2 compatible regex's describing the layers + * to draw below + * + * @return The first layer to be drawn above the radar products + */ +std::string FindMapSymbologyLayer(const QStringList& styleLayers, + const std::vector& drawBelow); + } // namespace maplibre } // namespace util } // namespace qt