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/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..b2089d08 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 { @@ -59,6 +63,8 @@ public: bool unitEnabled_ {false}; bool trimmingEnabled_ {false}; + + std::optional invalidTooltip_; }; template @@ -167,20 +173,24 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { if constexpr (std::is_same_v) { - QObject::connect(hotkeyEdit, - &ui::HotkeyEdit::KeySequenceChanged, - p->context_.get(), - [this](const QKeySequence& sequence) - { - std::string value { - sequence.toString().toStdString()}; + QObject::connect( + hotkeyEdit, + &ui::HotkeyEdit::KeySequenceChanged, + p->context_.get(), + [this, hotkeyEdit](const QKeySequence& sequence) + { + const std::string value {sequence.toString().toStdString()}; - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); - // TODO: Display invalid status - }); + hotkeyEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + hotkeyEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } } else if (QLineEdit* lineEdit = dynamic_cast(widget)) @@ -189,53 +199,64 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect(lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this](const QString& text) - { - QString trimmedText = - p->trimmingEnabled_ ? text.trimmed() : text; + QObject::connect( + lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this, lineEdit](const QString& text) + { + const QString trimmedText = + p->trimmingEnabled_ ? text.trimmed() : text; - // Map to value if required - std::string value {trimmedText.toStdString()}; - if (p->mapToValue_ != nullptr) - { - value = p->mapToValue_(value); - } + // Map to value if required + std::string value {trimmedText.toStdString()}; + if (p->mapToValue_ != nullptr) + { + value = p->mapToValue_(value); + } - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); - // TODO: Display invalid status - }); + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } else if constexpr (std::is_same_v) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect(lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this](const QString& text) - { - // Convert to a double - bool ok; - double value = text.toDouble(&ok); - if (ok) - { - // Attempt to stage the value - p->stagedValid_ = - p->variable_->StageValue(value); - p->UpdateResetButton(); - } - else - { - p->stagedValid_ = false; - p->UpdateResetButton(); - } - }); + QObject::connect( + lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this, lineEdit](const QString& text) + { + // Convert to a double + bool ok = false; + const double value = text.toDouble(&ok); + if (ok) + { + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); + } + else + { + p->stagedValid_ = false; + p->UpdateResetButton(); + } + + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } else if constexpr (std::is_same_v>) { @@ -245,7 +266,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) lineEdit, &QLineEdit::textEdited, p->context_.get(), - [this](const QString& text) + [this, lineEdit](const QString& text) { // Map to value if required T value {}; @@ -280,7 +301,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - // TODO: Display invalid status + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -343,7 +368,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) spinBox, &QSpinBox::valueChanged, p->context_.get(), - [this](int i) + [this, spinBox](int i) { const T value = p->variable_->GetValue(); const std::optional staged = p->variable_->GetStaged(); @@ -364,6 +389,12 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + spinBox->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + spinBox->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -389,7 +420,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) doubleSpinBox, &QDoubleSpinBox::valueChanged, p->context_.get(), - [this](double d) + [this, doubleSpinBox](double d) { if (p->unitEnabled_) { @@ -415,6 +446,13 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + doubleSpinBox->setStyleSheet( + p->stagedValid_ ? kValidStyleSheet_ : kInvalidStyleSheet_); + doubleSpinBox->setToolTip(p->invalidTooltip_ && + !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -500,6 +538,12 @@ void SettingsInterface::EnableTrimming(bool trimmingEnabled) p->trimmingEnabled_ = trimmingEnabled; } +template +void SettingsInterface::SetInvalidTooltip(std::optional tooltip) +{ + p->invalidTooltip_ = std::move(tooltip); +} + template template void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 3d7d9d85..0e42aca7 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(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..0bea7ffc --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp @@ -0,0 +1,124 @@ +#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_; +}; + +// TODO Duplicated form map_widget, Should probably be moved. +static bool match_layer(const std::string& pattern, const std::string& layer) +{ + // Perform case-insensitive matching + const RE2 re {"(?i)" + pattern}; + if (re.ok()) + { + return RE2::FullMatch(layer, re); + } + else + { + // Fall back to basic comparison if RE + // doesn't compile + return layer == pattern; + } +} + +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().GetValue(); + + const QStringList layerIds = map_->layerIds(); + self_->ui->layerListWidget->clear(); + self_->ui->layerListWidget->addItems(layerIds); + + for (int i = 0; i < self_->ui->layerListWidget->count(); i++) + { + auto* item = self_->ui->layerListWidget->item(i); + + if (match_layer(customStyleDrawLayer, item->text().toStdString())) + { + self_->ui->layerListWidget->setCurrentItem(item); + } + } + } +} + +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..8c03cf71 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace Ui +{ +class CustomLayerDialog; +} + +namespace scwx::qt::ui +{ + +class CustomLayerDialogImpl; + +class CustomLayerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CustomLayerDialog(const QMapLibre::Settings& settings, + QWidget* parent = nullptr); + ~CustomLayerDialog() override; + CustomLayerDialog(const CustomLayerDialog&) = delete; + CustomLayerDialog(CustomLayerDialog&&) = delete; + CustomLayerDialog& operator=(const CustomLayerDialog&) = delete; + CustomLayerDialog& operator=(CustomLayerDialog&&) = delete; + + 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..bf5c9180 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_ {std::move(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(const 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..b767aa0b 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(const 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