diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 1e70a3c7..4e6fffa0 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -18,6 +18,7 @@ Supercell Wx uses code from the following dependencies: | [CSS Color Parser](https://github.com/deanm/css-color-parser-js) | [MIT License](https://spdx.org/licenses/MIT.html) | Ported to C++ for MapLibre Native | | [Date](https://github.com/HowardHinnant/date) | [MIT License](https://spdx.org/licenses/MIT.html) | | [Dear ImGui](https://github.com/ocornut/imgui) | [MIT License](https://spdx.org/licenses/MIT.html) | +| [fontconfig](http://fontconfig.org/) | [MIT License](https://spdx.org/licenses/MIT.html) | | [FreeType](https://freetype.org/) | [Freetype Project License](https://spdx.org/licenses/FTL.html) | | [FreeType GL](https://github.com/rougier/freetype-gl) | [BSD 2-Clause with views sentence](https://spdx.org/licenses/BSD-2-Clause-Views.html) | | [GeographicLib](https://geographiclib.sourceforge.io/) | [MIT License](https://spdx.org/licenses/MIT.html) | diff --git a/conanfile.py b/conanfile.py index 4a803580..af9b5b90 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,6 +4,7 @@ class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.81.0", "cpr/1.9.3", + "fontconfig/2.14.2", "freetype/2.12.1", "geographiclib/1.52", "glew/2.2.0", diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b3d73964..4b8cadda 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost) +find_package(Fontconfig) find_package(Freetype) find_package(geographiclib) find_package(glm) @@ -73,7 +74,8 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/placefile_text.cpp source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) -set(HDR_MANAGER source/scwx/qt/manager/placefile_manager.hpp +set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp + source/scwx/qt/manager/placefile_manager.hpp source/scwx/qt/manager/radar_product_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp source/scwx/qt/manager/resource_manager.hpp @@ -81,7 +83,8 @@ set(HDR_MANAGER source/scwx/qt/manager/placefile_manager.hpp source/scwx/qt/manager/text_event_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) -set(SRC_MANAGER source/scwx/qt/manager/placefile_manager.cpp +set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp + source/scwx/qt/manager/placefile_manager.cpp source/scwx/qt/manager/radar_product_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp source/scwx/qt/manager/resource_manager.cpp @@ -157,6 +160,7 @@ set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/font_types.hpp source/scwx/qt/types/github_types.hpp + source/scwx/qt/types/imgui_font.hpp source/scwx/qt/types/map_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp @@ -164,6 +168,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/text_types.hpp) set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/github_types.cpp + source/scwx/qt/types/imgui_font.cpp source/scwx/qt/types/map_types.cpp source/scwx/qt/types/radar_product_record.cpp source/scwx/qt/types/text_event_key.cpp @@ -476,6 +481,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Boost::timer qmaplibregl $<$:opengl32> + Fontconfig::Fontconfig freetype-gl GeographicLib::GeographicLib glm::glm diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 18f2efee..b318e19f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -37,6 +39,7 @@ public: const std::string& text, const std::string& hoverText, boost::gil::rgba8_pixel_t color, + std::size_t fontNumber, float x, float y); @@ -62,6 +65,9 @@ public: std::mutex listMutex_ {}; std::vector> textList_ {}; std::vector> newList_ {}; + + std::vector> fonts_ {}; + std::vector> newFonts_ {}; }; PlacefileText::PlacefileText(const std::shared_ptr& context, @@ -155,6 +161,7 @@ void PlacefileText::Impl::RenderTextDrawItem( di->text_, di->hoverText_, di->color_, + std::clamp(di->fontNumber_, 1, 8), rotatedX + di->x_ + halfWidth_, rotatedY + di->y_ + halfHeight_); } @@ -165,6 +172,7 @@ void PlacefileText::Impl::RenderText( const std::string& text, const std::string& hoverText, boost::gil::rgba8_pixel_t color, + std::size_t fontNumber, float x, float y) { @@ -184,10 +192,12 @@ void PlacefileText::Impl::RenderText( ImGuiWindowFlags_NoBackground); // Render text + ImGui::PushFont(fonts_[fontNumber - 1]->font()); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(color[0], color[1], color[2], color[3])); ImGui::TextUnformatted(text.c_str()); ImGui::PopStyleColor(); + ImGui::PopFont(); // Store hover text for mouse picking pass if (!hoverText.empty() && ImGui::IsItemHovered()) @@ -231,6 +241,28 @@ void PlacefileText::StartText() p->newList_.clear(); } +void PlacefileText::SetFonts( + const boost::unordered_flat_map>& fonts) +{ + auto defaultFont = manager::FontManager::Instance().GetImGuiFont( + types::FontCategory::Default); + + // Valid font numbers are from 1 to 8, place in 0-based font vector + for (std::size_t i = 1; i <= 8; ++i) + { + auto it = fonts.find(i); + if (it != fonts.cend()) + { + p->newFonts_.push_back(it->second); + } + else + { + p->newFonts_.push_back(defaultFont); + } + } +} + void PlacefileText::AddText( const std::shared_ptr& di) { @@ -246,9 +278,11 @@ void PlacefileText::FinishText() // Swap text lists p->textList_.swap(p->newList_); + p->fonts_.swap(p->newFonts_); // Clear the new list p->newList_.clear(); + p->newFonts_.clear(); } } // namespace draw diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp index 979a802a..a8d23bc3 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include + namespace scwx { namespace qt @@ -44,6 +47,16 @@ public: */ void StartText(); + /** + * Configures the fonts for drawing the placefile text. + * + * @param [in] fonts A map of ImGui fonts + */ + void + SetFonts(const boost::unordered_flat_map>& + fonts); + /** * Adds placefile text to the internal draw list. * diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index b841b846..4c0bdac4 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -69,7 +69,7 @@ int main(int argc, char* argv[]) // Initialize application scwx::qt::config::RadarSite::Initialize(); - scwx::qt::manager::SettingsManager::Initialize(); + scwx::qt::manager::SettingsManager::Instance().Initialize(); scwx::qt::manager::ResourceManager::Initialize(); // Run Qt main loop @@ -89,7 +89,7 @@ int main(int argc, char* argv[]) // Shutdown application scwx::qt::manager::ResourceManager::Shutdown(); - scwx::qt::manager::SettingsManager::Shutdown(); + scwx::qt::manager::SettingsManager::Instance().Shutdown(); // Shutdown AWS SDK Aws::ShutdownAPI(awsSdkOptions); diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 4fff5ca0..756ca182 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -7,13 +7,14 @@ #include #include #include -#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -89,10 +90,8 @@ public: elevationButtonsChanged_ {false}, resizeElevationButtons_ {false} { - mapProvider_ = - map::GetMapProvider(manager::SettingsManager::general_settings() - .map_provider() - .GetValue()); + mapProvider_ = map::GetMapProvider( + settings::GeneralSettings::Instance().map_provider().GetValue()); const map::MapProviderInfo& mapProviderInfo = map::GetMapProviderInfo(mapProvider_); @@ -230,7 +229,7 @@ MainWindow::MainWindow(QWidget* parent) : ui->actionAlerts->setVisible(false); ui->menuDebug->menuAction()->setVisible( - manager::SettingsManager::general_settings().debug_enabled().GetValue()); + settings::GeneralSettings::Instance().debug_enabled().GetValue()); // Configure Resource Explorer Dock ui->resourceExplorerDock->setVisible(false); @@ -306,7 +305,7 @@ MainWindow::MainWindow(QWidget* parent) : // Update Dialog p->updateDialog_ = new ui::UpdateDialog(this); - auto& mapSettings = manager::SettingsManager::map_settings(); + auto& mapSettings = settings::MapSettings::Instance(); for (size_t i = 0; i < p->maps_.size(); i++) { p->SelectRadarProduct(p->maps_.at(i), @@ -582,7 +581,7 @@ void MainWindow::on_resourceTreeView_doubleClicked(const QModelIndex& index) void MainWindowImpl::AsyncSetup() { - auto& generalSettings = manager::SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); // Check for updates if (generalSettings.update_notifications_enabled().GetValue()) @@ -595,7 +594,7 @@ void MainWindowImpl::AsyncSetup() void MainWindowImpl::ConfigureMapLayout() { - auto& generalSettings = manager::SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); const int64_t gridWidth = generalSettings.grid_width().GetValue(); const int64_t gridHeight = generalSettings.grid_height().GetValue(); @@ -646,7 +645,7 @@ void MainWindowImpl::ConfigureMapLayout() void MainWindowImpl::ConfigureMapStyles() { const auto& mapProviderInfo = map::GetMapProviderInfo(mapProvider_); - auto& mapSettings = manager::SettingsManager::map_settings(); + auto& mapSettings = settings::MapSettings::Instance(); for (std::size_t i = 0; i < maps_.size(); i++) { @@ -897,8 +896,7 @@ void MainWindowImpl::ConnectOtherSignals() { if (maps_[i] == activeMap_) { - auto& mapSettings = - manager::SettingsManager::map_settings(); + auto& mapSettings = settings::MapSettings::Instance(); mapSettings.map_style(i).StageValue(text.toStdString()); break; } @@ -1075,7 +1073,7 @@ void MainWindowImpl::UpdateMapStyle(const std::string& styleName) { if (maps_[i] == activeMap_) { - auto& mapSettings = manager::SettingsManager::map_settings(); + auto& mapSettings = settings::MapSettings::Instance(); mapSettings.map_style(i).StageValue(styleName); break; } diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp new file mode 100644 index 00000000..2747fd4d --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -0,0 +1,512 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::font_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static const std::string kFcTrueType_ {"TrueType"}; + +struct FontRecord +{ + std::string family_ {}; + std::string style_ {}; + std::string filename_ {}; +}; + +typedef std::pair> FontRecordPair; + +template +struct FontRecordHash; + +template<> +struct FontRecordHash +{ + size_t operator()(const FontRecordPair& x) const; +}; + +class FontManager::Impl +{ +public: + explicit Impl(FontManager* self) : self_ {self} + { + InitializeFontCache(); + InitializeFontconfig(); + ConnectSignals(); + } + ~Impl() { FinalizeFontconfig(); } + + void ConnectSignals(); + void FinalizeFontconfig(); + void InitializeFontCache(); + void InitializeFontconfig(); + void UpdateImGuiFont(types::FontCategory fontCategory); + + const std::vector& GetRawFontData(const std::string& filename); + + static FontRecord MatchFontFile(const std::string& family, + const std::vector& styles); + + FontManager* self_; + + std::string fontCachePath_ {}; + + std::shared_mutex imguiFontAtlasMutex_ {}; + + std::uint64_t imguiFontsBuildCount_ {}; + + boost::unordered_flat_map, + FontRecordHash> + imguiFonts_ {}; + std::shared_mutex imguiFontsMutex_ {}; + + boost::unordered_flat_map> rawFontData_ {}; + std::mutex rawFontDataMutex_ {}; + + std::shared_ptr defaultFont_ {}; + boost::unordered_flat_map> + fontCategoryMap_ {}; + std::mutex fontCategoryMutex_ {}; + + boost::unordered_flat_set dirtyFonts_ {}; + std::mutex dirtyFontsMutex_ {}; + + boost::unordered_flat_map fontIds_ {}; +}; + +FontManager::FontManager() : p(std::make_unique(this)) {} + +FontManager::~FontManager() {}; + +void FontManager::Impl::ConnectSignals() +{ + auto& textSettings = settings::TextSettings::Instance(); + + for (auto fontCategory : types::FontCategoryIterator()) + { + textSettings.font_family(fontCategory) + .RegisterValueChangedCallback( + [this, fontCategory](const auto&) + { + std::unique_lock lock {dirtyFontsMutex_}; + dirtyFonts_.insert(fontCategory); + }); + textSettings.font_style(fontCategory) + .RegisterValueChangedCallback( + [this, fontCategory](const auto&) + { + std::unique_lock lock {dirtyFontsMutex_}; + dirtyFonts_.insert(fontCategory); + }); + textSettings.font_point_size(fontCategory) + .RegisterValueChangedCallback( + [this, fontCategory](const auto&) + { + std::unique_lock lock {dirtyFontsMutex_}; + dirtyFonts_.insert(fontCategory); + }); + } + + QObject::connect( + &SettingsManager::Instance(), + &SettingsManager::SettingsSaved, + self_, + [this]() + { + std::scoped_lock lock {dirtyFontsMutex_, fontCategoryMutex_}; + + for (auto fontCategory : dirtyFonts_) + { + UpdateImGuiFont(fontCategory); + } + + dirtyFonts_.clear(); + }); +} + +void FontManager::InitializeFonts() +{ + for (auto fontCategory : types::FontCategoryIterator()) + { + p->UpdateImGuiFont(fontCategory); + } +} + +void FontManager::Impl::UpdateImGuiFont(types::FontCategory fontCategory) +{ + auto& textSettings = settings::TextSettings::Instance(); + + auto family = textSettings.font_family(fontCategory).GetValue(); + auto styles = textSettings.font_style(fontCategory).GetValue(); + units::font_size::points size { + textSettings.font_point_size(fontCategory).GetValue()}; + + fontCategoryMap_.insert_or_assign( + fontCategory, self_->LoadImGuiFont(family, {styles}, size)); +} + +std::shared_mutex& FontManager::imgui_font_atlas_mutex() +{ + return p->imguiFontAtlasMutex_; +} + +std::uint64_t FontManager::imgui_fonts_build_count() const +{ + return p->imguiFontsBuildCount_; +} + +int FontManager::GetFontId(types::Font font) const +{ + auto it = p->fontIds_.find(font); + if (it != p->fontIds_.cend()) + { + return it->second; + } + return -1; +} + +std::shared_ptr +FontManager::GetImGuiFont(types::FontCategory fontCategory) +{ + std::unique_lock lock {p->fontCategoryMutex_}; + + auto it = p->fontCategoryMap_.find(fontCategory); + if (it != p->fontCategoryMap_.cend()) + { + return it->second; + } + + return p->defaultFont_; +} + +QFont FontManager::GetQFont(types::FontCategory fontCategory) +{ + auto& textSettings = settings::TextSettings::Instance(); + + auto family = textSettings.font_family(fontCategory).GetValue(); + auto styles = textSettings.font_style(fontCategory).GetValue(); + units::font_size::points size { + textSettings.font_point_size(fontCategory).GetValue()}; + + QFont font(QString::fromStdString(family)); + font.setStyleName(QString::fromStdString(styles)); + font.setPointSizeF(size.value()); + + return font; +} + +std::shared_ptr +FontManager::LoadImGuiFont(const std::string& family, + const std::vector& styles, + units::font_size::points size, + bool loadIfNotFound) +{ + const std::string styleString = fmt::format("{}", fmt::join(styles, " ")); + const std::string fontString = + fmt::format("{}-{}:{}", family, size.value(), styleString); + + logger_->debug("LoadFontResource: {}", fontString); + + FontRecord fontRecord = Impl::MatchFontFile(family, styles); + + // Only allow whole pixels, and clamp to 6-72 pt + units::font_size::pixels pixels {size}; + units::font_size::pixels imFontSize { + std::clamp(static_cast(pixels.value()), 8, 96)}; + auto imguiFontKey = std::make_pair(fontRecord, imFontSize); + + // Search for a loaded ImGui font + { + std::shared_lock imguiFontLock {p->imguiFontsMutex_}; + + // Search for the associated ImGui font + auto it = p->imguiFonts_.find(imguiFontKey); + if (it != p->imguiFonts_.end()) + { + return it->second; + } + + // No ImGui font was found, we need to create one + } + + // No font was found, return an empty shared pointer if not loading + if (!loadIfNotFound) + { + return nullptr; + } + + // Get raw font data + const auto& rawFontData = p->GetRawFontData(fontRecord.filename_); + + // The font atlas mutex might already be locked within an ImGui render frame. + // Lock the font atlas mutex before the fonts mutex to prevent deadlock. + std::unique_lock imguiFontAtlasLock {p->imguiFontAtlasMutex_}; + std::unique_lock imguiFontsLock {p->imguiFontsMutex_}; + + // Search for the associated ImGui font again, to prevent loading the same + // font twice + auto it = p->imguiFonts_.find(imguiFontKey); + if (it != p->imguiFonts_.end()) + { + return it->second; + } + + // Define a name for the ImGui font + std::string fontName; + try + { + fontName = fmt::format( + "{}:{}", + std::filesystem::path(fontRecord.filename_).filename().string(), + imFontSize.value()); + } + catch (const std::exception& ex) + { + logger_->warn(ex.what()); + fontName = fmt::format("{}:{}", fontRecord.filename_, imFontSize.value()); + } + + // Create an ImGui font + std::shared_ptr imguiFont = + std::make_shared(fontName, rawFontData, imFontSize); + + // Store the ImGui font + p->imguiFonts_.insert_or_assign(imguiFontKey, imguiFont); + + // Increment ImGui font build count + ++p->imguiFontsBuildCount_; + + // Return the ImGui font + return imguiFont; +} + +const std::vector& +FontManager::Impl::GetRawFontData(const std::string& filename) +{ + std::unique_lock rawFontDataLock {rawFontDataMutex_}; + + auto it = rawFontData_.find(filename); + if (it != rawFontData_.end()) + { + // Raw font data has already been loaded + return it->second; + } + + // Raw font data needs to be loaded + std::basic_ifstream ifs {filename, std::ios::binary}; + ifs.seekg(0, std::ios_base::end); + std::size_t dataSize = ifs.tellg(); + ifs.seekg(0, std::ios_base::beg); + + // Store the font data in a buffer + std::vector buffer {}; + buffer.reserve(dataSize); + std::copy(std::istreambuf_iterator(ifs), + std::istreambuf_iterator(), + std::back_inserter(buffer)); + + // Place the buffer in the cache + auto result = rawFontData_.emplace(filename, std::move(buffer)); + + // Return the cached buffer + return result.first->second; +} + +void FontManager::LoadApplicationFont(types::Font font, + const std::string& filename) +{ + // If the font cache failed to create, don't attempt to cache any fonts + if (p->fontCachePath_.empty()) + { + return; + } + + // Make a copy of the font in the cache (if it doesn't exist) + QFile fontFile(QString::fromStdString(filename)); + QFileInfo fontFileInfo(fontFile); + + QFile cacheFile(QString::fromStdString(p->fontCachePath_) + + fontFileInfo.fileName()); + QFileInfo cacheFileInfo(cacheFile); + std::string cacheFilename = cacheFile.fileName().toStdString(); + + if (fontFile.exists()) + { + // If the file has not been cached, or the font file size has changed + if (!cacheFile.exists() || fontFileInfo.size() != cacheFileInfo.size()) + { + logger_->info("Caching font: {}", filename); + if (!fontFile.copy(cacheFile.fileName())) + { + logger_->error("Could not cache font: {}", filename); + return; + } + } + } + else + { + logger_->error("Font does not exist: {}", filename); + return; + } + + // Load the file into the Qt Font Database + int fontId = + QFontDatabase::addApplicationFont(QString::fromStdString(cacheFilename)); + p->fontIds_.emplace(font, fontId); + + // Load the file into fontconfig + FcBool result = FcConfigAppFontAddFile( + nullptr, reinterpret_cast(cacheFilename.c_str())); + if (!result) + { + logger_->error("Could not load font into fontconfig database", filename); + } +} + +void FontManager::Impl::InitializeFontCache() +{ + std::string cachePath { + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + .toStdString() + + "/fonts"}; + + fontCachePath_ = cachePath + "/"; + + if (!std::filesystem::exists(cachePath)) + { + std::error_code error; + if (!std::filesystem::create_directories(cachePath, error)) + { + logger_->error("Unable to create font cache directory: \"{}\" ({})", + cachePath, + error.message()); + fontCachePath_.clear(); + } + } +} + +void FontManager::Impl::InitializeFontconfig() +{ + FcConfig* fcConfig = FcInitLoadConfigAndFonts(); + FcConfigSetCurrent(fcConfig); +} + +void FontManager::Impl::FinalizeFontconfig() +{ + FcFini(); +} + +FontRecord +FontManager::Impl::MatchFontFile(const std::string& family, + const std::vector& styles) +{ + const std::string styleString = fmt::format("{}", fmt::join(styles, " ")); + const std::string fontString = fmt::format("{}:{}", family, styleString); + + // Build fontconfig pattern + FcPattern* pattern = FcPatternCreate(); + + FcPatternAddString( + pattern, FC_FAMILY, reinterpret_cast(family.c_str())); + FcPatternAddString(pattern, + FC_FONTFORMAT, + reinterpret_cast(kFcTrueType_.c_str())); + + if (!styles.empty()) + { + FcPatternAddString(pattern, + FC_STYLE, + reinterpret_cast(styleString.c_str())); + } + + // Perform font pattern match substitution + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + // Find matching font + FcResult result; + FcPattern* match = FcFontMatch(nullptr, pattern, &result); + FontRecord record {}; + + if (match != nullptr) + { + FcChar8* fcFamily; + FcChar8* fcStyle; + FcChar8* fcFile; + + // Match was found, get properties + if (FcPatternGetString(match, FC_FAMILY, 0, &fcFamily) == FcResultMatch && + FcPatternGetString(match, FC_STYLE, 0, &fcStyle) == FcResultMatch && + FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch) + { + record.family_ = reinterpret_cast(fcFamily); + record.style_ = reinterpret_cast(fcStyle); + record.filename_ = reinterpret_cast(fcFile); + + logger_->debug("Found matching font: {}:{} ({})", + record.family_, + record.style_, + record.filename_); + } + } + + if (record.filename_.empty()) + { + logger_->warn("Could not find matching font: {}", fontString); + } + + // Cleanup + FcPatternDestroy(match); + FcPatternDestroy(pattern); + + return record; +} + +FontManager& FontManager::Instance() +{ + static FontManager instance_ {}; + return instance_; +} + +size_t FontRecordHash::operator()(const FontRecordPair& x) const +{ + size_t seed = 0; + boost::hash_combine(seed, x.first.family_); + boost::hash_combine(seed, x.first.style_); + boost::hash_combine(seed, x.first.filename_); + boost::hash_combine(seed, x.second.value()); + return seed; +} + +bool operator==(const FontRecord& lhs, const FontRecord& rhs) +{ + return lhs.family_ == rhs.family_ && // + lhs.style_ == rhs.style_ && // + lhs.filename_ == rhs.filename_; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.hpp b/scwx-qt/source/scwx/qt/manager/font_manager.hpp new file mode 100644 index 00000000..4cc21083 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/font_manager.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class FontManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(FontManager) + +public: + explicit FontManager(); + ~FontManager(); + + std::shared_mutex& imgui_font_atlas_mutex(); + std::uint64_t imgui_fonts_build_count() const; + + int GetFontId(types::Font font) const; + std::shared_ptr + GetImGuiFont(types::FontCategory fontCategory); + std::shared_ptr + LoadImGuiFont(const std::string& family, + const std::vector& styles, + units::font_size::points size, + bool loadIfNotFound = true); + + void LoadApplicationFont(types::Font font, const std::string& filename); + void InitializeFonts(); + + static QFont GetQFont(types::FontCategory fontCategory); + + static FontManager& Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index 77344524..67bc3f1c 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -51,8 +52,11 @@ public: void ReadPlacefileSettings(); void WritePlacefileSettings(); + static boost::unordered_flat_map> + LoadFontResources(const std::shared_ptr& placefile); static std::vector> - LoadResources(const std::shared_ptr& placefile); + LoadImageResources(const std::shared_ptr& placefile); boost::asio::thread_pool threadPool_ {1u}; @@ -63,7 +67,7 @@ public: std::shared_ptr radarSite_ {}; std::vector> placefileRecords_ {}; - std::unordered_map> + boost::unordered_flat_map> placefileRecordMap_ {}; std::shared_mutex placefileRecordLock_ {}; }; @@ -134,6 +138,10 @@ public: std::mutex refreshMutex_ {}; std::mutex timerMutex_ {}; + boost::unordered_flat_map> + fonts_ {}; + std::mutex fontsMutex_ {}; + std::vector> images_ {}; std::string lastRadarSite_ {}; @@ -208,6 +216,20 @@ PlacefileManager::placefile(const std::string& name) return nullptr; } +boost::unordered_flat_map> +PlacefileManager::placefile_fonts(const std::string& name) +{ + std::shared_lock lock(p->placefileRecordLock_); + + auto it = p->placefileRecordMap_.find(name); + if (it != p->placefileRecordMap_.cend()) + { + std::unique_lock fontsLock {it->second->fontsMutex_}; + return it->second->fonts_; + } + return {}; +} + void PlacefileManager::set_placefile_enabled(const std::string& name, bool enabled) { @@ -278,6 +300,7 @@ void PlacefileManager::set_placefile_url(const std::string& name, auto placefileRecord = it->second; placefileRecord->name_ = normalizedUrl; placefileRecord->placefile_ = nullptr; + placefileRecord->fonts_.clear(); placefileRecord->images_.clear(); p->placefileRecordMap_.erase(it); p->placefileRecordMap_.insert_or_assign(normalizedUrl, placefileRecord); @@ -587,7 +610,8 @@ void PlacefileManager::Impl::PlacefileRecord::Update() if (updatedPlacefile != nullptr) { // Load placefile resources - auto newImages = Impl::LoadResources(updatedPlacefile); + auto newFonts = Impl::LoadFontResources(updatedPlacefile); + auto newImages = Impl::LoadImageResources(updatedPlacefile); // Check the name matches, in case the name updated if (name_ == name) @@ -597,6 +621,13 @@ void PlacefileManager::Impl::PlacefileRecord::Update() title_ = placefile_->title(); lastUpdateTime_ = std::chrono::system_clock::now(); + // Update font resources + { + std::unique_lock fontsLock {fontsMutex_}; + fonts_.swap(newFonts); + newFonts.clear(); + } + // Update image resources images_.swap(newImages); newImages.clear(); @@ -684,8 +715,38 @@ std::shared_ptr PlacefileManager::Instance() return placefileManager; } +boost::unordered_flat_map> +PlacefileManager::Impl::LoadFontResources( + const std::shared_ptr& placefile) +{ + boost::unordered_flat_map> + imGuiFonts {}; + auto fonts = placefile->fonts(); + + for (auto& font : fonts) + { + units::font_size::pixels size {font.second->pixels_}; + std::vector styles {}; + + if (font.second->IsBold()) + { + styles.push_back("bold"); + } + if (font.second->IsItalic()) + { + styles.push_back("italic"); + } + + auto imGuiFont = FontManager::Instance().LoadImGuiFont( + font.second->face_, styles, size); + imGuiFonts.emplace(font.first, std::move(imGuiFont)); + } + + return imGuiFonts; +} + std::vector> -PlacefileManager::Impl::LoadResources( +PlacefileManager::Impl::LoadImageResources( const std::shared_ptr& placefile) { const auto iconFiles = placefile->icon_files(); diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp index 0567265f..ee9bf3a4 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp @@ -2,8 +2,10 @@ #include #include +#include #include +#include namespace scwx { @@ -24,6 +26,8 @@ public: bool placefile_thresholded(const std::string& name); std::string placefile_title(const std::string& name); std::shared_ptr placefile(const std::string& name); + boost::unordered_flat_map> + placefile_fonts(const std::string& name); void set_placefile_enabled(const std::string& name, bool enabled); void set_placefile_thresholded(const std::string& name, bool thresholded); diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 1e694905..3048fc6c 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -1,14 +1,13 @@ #include +#include #include #include -#include #include #include #include #include -#include #include namespace scwx @@ -26,14 +25,11 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static void LoadFonts(); static void LoadTextures(); -static const std::unordered_map fontNames_ { +static const std::vector> fontNames_ { {types::Font::din1451alt, ":/res/fonts/din1451alt.ttf"}, {types::Font::din1451alt_g, ":/res/fonts/din1451alt_g.ttf"}, {types::Font::Inconsolata_Regular, ":/res/fonts/Inconsolata-Regular.ttf"}}; -static std::unordered_map fontIds_ {}; -static std::unordered_map> fonts_ {}; - void Initialize() { config::CountyDatabase::Initialize(); @@ -44,26 +40,6 @@ void Initialize() void Shutdown() {} -int FontId(types::Font font) -{ - auto it = fontIds_.find(font); - if (it != fontIds_.cend()) - { - return it->second; - } - return -1; -} - -std::shared_ptr Font(types::Font font) -{ - auto it = fonts_.find(font); - if (it != fonts_.cend()) - { - return it->second; - } - return nullptr; -} - std::shared_ptr LoadImageResource(const std::string& urlString) { @@ -102,18 +78,14 @@ LoadImageResources(const std::vector& urlStrings) static void LoadFonts() { + auto& fontManager = FontManager::Instance(); + for (auto& fontName : fontNames_) { - int fontId = QFontDatabase::addApplicationFont( - QString::fromStdString(fontName.second)); - fontIds_.emplace(fontName.first, fontId); - - auto font = util::Font::Create(fontName.second); - fonts_.emplace(fontName.first, font); + fontManager.LoadApplicationFont(fontName.first, fontName.second); } - ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); - fontAtlas->AddFontDefault(); + fontManager.InitializeFonts(); } static void LoadTextures() diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index 7b8003c9..00658891 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include @@ -19,9 +18,6 @@ namespace ResourceManager void Initialize(); void Shutdown(); -int FontId(types::Font font); -std::shared_ptr Font(types::Font font); - std::shared_ptr LoadImageResource(const std::string& urlString); std::vector> diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index 7c973ca4..a4c0f4a3 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include #include #include @@ -18,21 +21,33 @@ namespace qt { namespace manager { -namespace SettingsManager -{ static const std::string logPrefix_ = "scwx::qt::manager::settings_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static boost::json::value ConvertSettingsToJson(); -static void GenerateDefaultSettings(); -static bool LoadSettings(const boost::json::object& settingsJson); -static void ValidateSettings(); +class SettingsManager::Impl +{ +public: + explicit Impl(SettingsManager* self) : self_ {self} {} + ~Impl() = default; -static bool initialized_ {false}; -static std::string settingsPath_ {}; + void ValidateSettings(); -void Initialize() + static boost::json::value ConvertSettingsToJson(); + static void GenerateDefaultSettings(); + static bool LoadSettings(const boost::json::object& settingsJson); + + SettingsManager* self_; + + bool initialized_ {false}; + std::string settingsPath_ {}; +}; + +SettingsManager::SettingsManager() : p(std::make_unique(this)) {} + +SettingsManager::~SettingsManager() {}; + +void SettingsManager::Initialize() { std::string appDataPath { QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) @@ -47,14 +62,14 @@ void Initialize() } } - settingsPath_ = appDataPath + "/settings.json"; - initialized_ = true; + p->settingsPath_ = appDataPath + "/settings.json"; + p->initialized_ = true; - ReadSettings(settingsPath_); - ValidateSettings(); + ReadSettings(p->settingsPath_); + p->ValidateSettings(); } -void ReadSettings(const std::string& settingsPath) +void SettingsManager::ReadSettings(const std::string& settingsPath) { boost::json::value settingsJson = nullptr; @@ -65,39 +80,41 @@ void ReadSettings(const std::string& settingsPath) if (settingsJson == nullptr || !settingsJson.is_object()) { - GenerateDefaultSettings(); - settingsJson = ConvertSettingsToJson(); + Impl::GenerateDefaultSettings(); + settingsJson = Impl::ConvertSettingsToJson(); util::json::WriteJsonFile(settingsPath, settingsJson); } else { - bool jsonDirty = LoadSettings(settingsJson.as_object()); + bool jsonDirty = Impl::LoadSettings(settingsJson.as_object()); if (jsonDirty) { - settingsJson = ConvertSettingsToJson(); + settingsJson = Impl::ConvertSettingsToJson(); util::json::WriteJsonFile(settingsPath, settingsJson); } }; } -void SaveSettings() +void SettingsManager::SaveSettings() { - if (initialized_) + if (p->initialized_) { logger_->info("Saving settings"); - boost::json::value settingsJson = ConvertSettingsToJson(); - util::json::WriteJsonFile(settingsPath_, settingsJson); + boost::json::value settingsJson = Impl::ConvertSettingsToJson(); + util::json::WriteJsonFile(p->settingsPath_, settingsJson); + + Q_EMIT SettingsSaved(); } } -void Shutdown() +void SettingsManager::Shutdown() { bool dataChanged = false; - dataChanged |= general_settings().Shutdown(); - dataChanged |= map_settings().Shutdown(); + dataChanged |= settings::GeneralSettings::Instance().Shutdown(); + dataChanged |= settings::MapSettings::Instance().Shutdown(); dataChanged |= settings::UiSettings::Instance().Shutdown(); if (dataChanged) @@ -106,70 +123,53 @@ void Shutdown() } } -settings::GeneralSettings& general_settings() -{ - static settings::GeneralSettings generalSettings_; - return generalSettings_; -} - -settings::MapSettings& map_settings() -{ - static settings::MapSettings mapSettings_; - return mapSettings_; -} - -settings::PaletteSettings& palette_settings() -{ - static settings::PaletteSettings paletteSettings_; - return paletteSettings_; -} - -static boost::json::value ConvertSettingsToJson() +boost::json::value SettingsManager::Impl::ConvertSettingsToJson() { boost::json::object settingsJson; - general_settings().WriteJson(settingsJson); - map_settings().WriteJson(settingsJson); - palette_settings().WriteJson(settingsJson); + settings::GeneralSettings::Instance().WriteJson(settingsJson); + settings::MapSettings::Instance().WriteJson(settingsJson); + settings::PaletteSettings::Instance().WriteJson(settingsJson); settings::TextSettings::Instance().WriteJson(settingsJson); settings::UiSettings::Instance().WriteJson(settingsJson); return settingsJson; } -static void GenerateDefaultSettings() +void SettingsManager::Impl::GenerateDefaultSettings() { logger_->info("Generating default settings"); - general_settings().SetDefaults(); - map_settings().SetDefaults(); - palette_settings().SetDefaults(); + settings::GeneralSettings::Instance().SetDefaults(); + settings::MapSettings::Instance().SetDefaults(); + settings::PaletteSettings::Instance().SetDefaults(); settings::TextSettings::Instance().SetDefaults(); settings::UiSettings::Instance().SetDefaults(); } -static bool LoadSettings(const boost::json::object& settingsJson) +bool SettingsManager::Impl::LoadSettings( + const boost::json::object& settingsJson) { logger_->info("Loading settings"); bool jsonDirty = false; - jsonDirty |= !general_settings().ReadJson(settingsJson); - jsonDirty |= !map_settings().ReadJson(settingsJson); - jsonDirty |= !palette_settings().ReadJson(settingsJson); + jsonDirty |= !settings::GeneralSettings::Instance().ReadJson(settingsJson); + jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson); + jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::TextSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::UiSettings::Instance().ReadJson(settingsJson); return jsonDirty; } -static void ValidateSettings() +void SettingsManager::Impl::ValidateSettings() { logger_->debug("Validating settings"); bool settingsChanged = false; - auto& generalSettings = general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); // Validate map provider std::string mapProviderName = generalSettings.map_provider().GetValue(); @@ -200,11 +200,16 @@ static void ValidateSettings() if (settingsChanged) { - SaveSettings(); + self_->SaveSettings(); } } -} // namespace SettingsManager +SettingsManager& SettingsManager::Instance() +{ + static SettingsManager instance_ {}; + return instance_; +} + } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.hpp b/scwx-qt/source/scwx/qt/manager/settings_manager.hpp index ed05ca1e..254ea4c8 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.hpp @@ -1,8 +1,9 @@ #pragma once -#include -#include -#include +#include +#include + +#include namespace scwx { @@ -10,19 +11,31 @@ namespace qt { namespace manager { -namespace SettingsManager + +class SettingsManager : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(SettingsManager) -void Initialize(); -void ReadSettings(const std::string& settingsPath); -void SaveSettings(); -void Shutdown(); +public: + explicit SettingsManager(); + ~SettingsManager(); -settings::GeneralSettings& general_settings(); -settings::MapSettings& map_settings(); -settings::PaletteSettings& palette_settings(); + void Initialize(); + void ReadSettings(const std::string& settingsPath); + void SaveSettings(); + void Shutdown(); + + static SettingsManager& Instance(); + +signals: + void SettingsSaved(); + +private: + class Impl; + std::unique_ptr p; +}; -} // namespace SettingsManager } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 8e79a552..f7fc3f48 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -39,7 +39,7 @@ class TimelineManager::Impl public: explicit Impl(TimelineManager* self) : self_ {self} { - auto& generalSettings = SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); loopDelay_ = std::chrono::milliseconds(generalSettings.loop_delay().GetValue()); diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index f769231c..044cfb00 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include #include #include @@ -394,7 +394,7 @@ static void AddAlertLayer(std::shared_ptr map, const QString& beforeLayer) { settings::PaletteSettings& paletteSettings = - manager::SettingsManager::palette_settings(); + settings::PaletteSettings::Instance(); QString sourceId = GetSourceId(phenomenon, alertActive); QString idSuffix = GetSuffix(phenomenon, alertActive); diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index 2a431614..df4bba08 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include @@ -128,12 +128,10 @@ std::string GetMapProviderApiKey(MapProvider mapProvider) switch (mapProvider) { case MapProvider::Mapbox: - return manager::SettingsManager::general_settings() - .mapbox_api_key() - .GetValue(); + return settings::GeneralSettings::Instance().mapbox_api_key().GetValue(); case MapProvider::MapTiler: - return manager::SettingsManager::general_settings() + return settings::GeneralSettings::Instance() .maptiler_api_key() .GetValue(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 4f279ad3..a54c1a5e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1,8 +1,8 @@ #include #include +#include #include #include -#include #include #include #include @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -80,8 +82,7 @@ public: prevBearing_ {0.0}, prevPitch_ {0.0} { - auto& generalSettings = - scwx::qt::manager::SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); SetRadarSite(generalSettings.default_radar_site().GetValue()); @@ -123,6 +124,7 @@ public: std::shared_ptr layer, const std::string& before = {}); void ConnectSignals(); + void ImGuiCheckFonts(); void InitializeNewRadarProductView(const std::string& colorPalette); void RadarProductManagerConnect(); void RadarProductManagerDisconnect(); @@ -154,6 +156,7 @@ public: ImGuiContext* imGuiContext_; std::string imGuiContextName_; bool imGuiRendererInitialized_; + std::uint64_t imGuiFontsBuildCount_ {}; std::shared_ptr placefileManager_ { manager::PlacefileManager::Instance()}; @@ -980,9 +983,15 @@ void MapWidget::initializeGL() makeCurrent(); p->context_->gl().initializeOpenGLFunctions(); + // Lock ImGui font atlas prior to new ImGui frame + std::shared_lock imguiFontAtlasLock { + manager::FontManager::Instance().imgui_font_atlas_mutex()}; + // Initialize ImGui OpenGL3 backend ImGui::SetCurrentContext(p->imGuiContext_); ImGui_ImplOpenGL3_Init(); + p->imGuiFontsBuildCount_ = + manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; p->map_.reset( @@ -1023,16 +1032,27 @@ void MapWidget::initializeGL() void MapWidget::paintGL() { + auto defaultFont = manager::FontManager::Instance().GetImGuiFont( + types::FontCategory::Default); + p->frameDraws_++; // Setup ImGui Frame ImGui::SetCurrentContext(p->imGuiContext_); + // Lock ImGui font atlas prior to new ImGui frame + std::shared_lock imguiFontAtlasLock { + manager::FontManager::Instance().imgui_font_atlas_mutex()}; + // Start ImGui Frame ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); + p->ImGuiCheckFonts(); ImGui::NewFrame(); + // Set default font + ImGui::PushFont(defaultFont->font()); + // Update pixel ratio p->context_->set_pixel_ratio(pixelRatio()); @@ -1055,14 +1075,36 @@ void MapWidget::paintGL() p->lastItemPicked_ = false; } + // Pop default font + ImGui::PopFont(); + // Render ImGui Frame ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + // Unlock ImGui font atlas after rendering + imguiFontAtlasLock.unlock(); + // Paint complete Q_EMIT WidgetPainted(); } +void MapWidgetImpl::ImGuiCheckFonts() +{ + // Update ImGui Fonts if required + std::uint64_t currentImGuiFontsBuildCount = + manager::FontManager::Instance().imgui_fonts_build_count(); + + if (imGuiFontsBuildCount_ != currentImGuiFontsBuildCount || + !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + { + ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplOpenGL3_CreateFontsTexture(); + } + + imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; +} + void MapWidgetImpl::RunMousePicking() { const QMapLibreGL::CustomLayerRenderParameters params = @@ -1203,7 +1245,7 @@ void MapWidgetImpl::InitializeNewRadarProductView( auto radarProductView = context_->radar_product_view(); std::string colorTableFile = - manager::SettingsManager::palette_settings() + settings::PaletteSettings::Instance() .palette(colorPalette) .GetValue(); if (!colorTableFile.empty()) diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index f29d63e4..3f6e5961 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -197,6 +197,8 @@ void PlacefileLayer::ReloadData() p->placefileIcons_->SetIconFiles(placefile->icon_files(), placefile->name()); + p->placefileText_->SetFonts( + placefileManager->placefile_fonts(p->placefileName_)); for (auto& drawItem : placefile->GetDrawItems()) { diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 8f2d3e74..bd94acef 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -16,10 +16,10 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::general_settings"; -class GeneralSettingsImpl +class GeneralSettings::Impl { public: - explicit GeneralSettingsImpl() + explicit Impl() { std::string defaultDefaultAlertActionValue = types::GetAlertActionName(types::AlertAction::Go); @@ -102,7 +102,7 @@ public: { return !value.empty(); }); } - ~GeneralSettingsImpl() {} + ~Impl() {} SettingsVariable debugEnabled_ {"debug_enabled"}; SettingsVariable defaultAlertAction_ {"default_alert_action"}; @@ -120,7 +120,7 @@ public: }; GeneralSettings::GeneralSettings() : - SettingsCategory("general"), p(std::make_unique()) + SettingsCategory("general"), p(std::make_unique()) { RegisterVariables({&p->debugEnabled_, &p->defaultAlertAction_, @@ -221,6 +221,12 @@ bool GeneralSettings::Shutdown() return dataChanged; } +GeneralSettings& GeneralSettings::Instance() +{ + static GeneralSettings generalSettings_; + return generalSettings_; +} + bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) { return (lhs.p->debugEnabled_ == rhs.p->debugEnabled_ && diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 375d887e..ba58676f 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -13,8 +13,6 @@ namespace qt namespace settings { -class GeneralSettingsImpl; - class GeneralSettings : public SettingsCategory { public: @@ -41,13 +39,16 @@ public: SettingsVariable& maptiler_api_key() const; SettingsVariable& update_notifications_enabled() const; + static GeneralSettings& Instance(); + friend bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs); bool Shutdown(); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 8edd1c31..2d8e93b0 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -35,7 +35,7 @@ static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; -class MapSettingsImpl +class MapSettings::Impl { public: struct MapData @@ -47,7 +47,7 @@ public: SettingsVariable radarProduct_ {kRadarProductName_}; }; - explicit MapSettingsImpl() + explicit Impl() { for (std::size_t i = 0; i < kCount_; i++) { @@ -101,7 +101,7 @@ public: } } - ~MapSettingsImpl() {} + ~Impl() {} void SetDefaults(std::size_t i) { @@ -111,12 +111,30 @@ public: map_[i].radarProduct_.SetValueToDefault(); } + friend void tag_invoke(boost::json::value_from_tag, + boost::json::value& jv, + const MapData& data) + { + jv = {{kMapStyleName_, data.mapStyle_.GetValue()}, + {kRadarSiteName_, data.radarSite_.GetValue()}, + {kRadarProductGroupName_, data.radarProductGroup_.GetValue()}, + {kRadarProductName_, data.radarProduct_.GetValue()}}; + } + + friend bool operator==(const MapData& lhs, const MapData& rhs) + { + return (lhs.mapStyle_ == rhs.mapStyle_ && // + lhs.radarSite_ == rhs.radarSite_ && + lhs.radarProductGroup_ == rhs.radarProductGroup_ && + lhs.radarProduct_ == rhs.radarProduct_); + } + std::array map_ {}; std::vector variables_ {}; }; MapSettings::MapSettings() : - SettingsCategory("maps"), p(std::make_unique()) + SettingsCategory("maps"), p(std::make_unique()) { RegisterVariables(p->variables_); SetDefaults(); @@ -161,7 +179,7 @@ bool MapSettings::Shutdown() // Commit settings that are managed separate from the settings dialog for (std::size_t i = 0; i < kCount_; ++i) { - MapSettingsImpl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_[i]; dataChanged |= mapRecordSettings.mapStyle_.Commit(); } @@ -184,7 +202,7 @@ bool MapSettings::ReadJson(const boost::json::object& json) if (i < mapArray.size() && mapArray.at(i).is_object()) { const boost::json::object& mapRecord = mapArray.at(i).as_object(); - MapSettingsImpl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_[i]; // Load JSON Elements validated &= mapRecordSettings.mapStyle_.ReadValue(mapRecord); @@ -234,14 +252,10 @@ void MapSettings::WriteJson(boost::json::object& json) const json.insert_or_assign(name(), object); } -void tag_invoke(boost::json::value_from_tag, - boost::json::value& jv, - const MapSettingsImpl::MapData& data) +MapSettings& MapSettings::Instance() { - jv = {{kMapStyleName_, data.mapStyle_.GetValue()}, - {kRadarSiteName_, data.radarSite_.GetValue()}, - {kRadarProductGroupName_, data.radarProductGroup_.GetValue()}, - {kRadarProductName_, data.radarProduct_.GetValue()}}; + static MapSettings mapSettings_; + return mapSettings_; } bool operator==(const MapSettings& lhs, const MapSettings& rhs) @@ -249,15 +263,6 @@ bool operator==(const MapSettings& lhs, const MapSettings& rhs) return (lhs.p->map_ == rhs.p->map_); } -bool operator==(const MapSettingsImpl::MapData& lhs, - const MapSettingsImpl::MapData& rhs) -{ - return (lhs.mapStyle_ == rhs.mapStyle_ && // - lhs.radarSite_ == rhs.radarSite_ && - lhs.radarProductGroup_ == rhs.radarProductGroup_ && - lhs.radarProduct_ == rhs.radarProduct_); -} - } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index fd0c74dd..c8726491 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -13,8 +13,6 @@ namespace qt namespace settings { -class MapSettingsImpl; - class MapSettings : public SettingsCategory { public: @@ -52,10 +50,13 @@ public: */ void WriteJson(boost::json::object& json) const override; + static MapSettings& Instance(); + friend bool operator==(const MapSettings& lhs, const MapSettings& rhs); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 8edf2cf2..f041c078 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -72,10 +72,10 @@ static const std::map< static const std::string kDefaultKey_ {"???"}; static const awips::Phenomenon kDefaultPhenomenon_ {awips::Phenomenon::Marine}; -class PaletteSettingsImpl +class PaletteSettings::Impl { public: - explicit PaletteSettingsImpl() + explicit Impl() { for (const auto& name : kPaletteKeys_) { @@ -120,7 +120,7 @@ public: } } - ~PaletteSettingsImpl() {} + ~Impl() {} static bool ValidateColor(const std::string& value); @@ -132,14 +132,14 @@ public: std::vector variables_ {}; }; -bool PaletteSettingsImpl::ValidateColor(const std::string& value) +bool PaletteSettings::Impl::ValidateColor(const std::string& value) { static const std::regex re {"#[0-9A-Za-z]{8}"}; return std::regex_match(value, re); } PaletteSettings::PaletteSettings() : - SettingsCategory("palette"), p(std::make_unique()) + SettingsCategory("palette"), p(std::make_unique()) { RegisterVariables(p->variables_); SetDefaults(); @@ -200,6 +200,12 @@ const std::vector& PaletteSettings::alert_phenomena() return kAlertPhenomena_; } +PaletteSettings& PaletteSettings::Instance() +{ + static PaletteSettings paletteSettings_; + return paletteSettings_; +} + bool operator==(const PaletteSettings& lhs, const PaletteSettings& rhs) { return lhs.p->palette_ == rhs.p->palette_; diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index 948decf5..c0f7985a 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -14,8 +14,6 @@ namespace qt namespace settings { -class PaletteSettingsImpl; - class PaletteSettings : public SettingsCategory { public: @@ -34,11 +32,14 @@ public: static const std::vector& alert_phenomena(); + static PaletteSettings& Instance(); + friend bool operator==(const PaletteSettings& lhs, const PaletteSettings& rhs); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 75daf41d..e6c929f8 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -2,6 +2,8 @@ #include #include +#include + namespace scwx { namespace qt @@ -21,6 +23,8 @@ public: const std::string name_; + std::vector>> + subcategoryArrays_; std::vector variables_; }; @@ -41,6 +45,16 @@ std::string SettingsCategory::name() const void SettingsCategory::SetDefaults() { + // Set subcategory array defaults + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + subcategory->SetDefaults(); + } + } + + // Set variable defaults for (auto& variable : p->variables_) { variable->SetValueToDefault(); @@ -57,6 +71,47 @@ bool SettingsCategory::ReadJson(const boost::json::object& json) { const boost::json::object& object = value->as_object(); + // Read subcategory arrays + for (auto& subcategoryArray : p->subcategoryArrays_) + { + const boost::json::value* arrayValue = + object.if_contains(subcategoryArray.first); + + if (arrayValue != nullptr && arrayValue->is_object()) + { + const boost::json::object& arrayObject = arrayValue->as_object(); + + for (auto& subcategory : subcategoryArray.second) + { + validated &= subcategory->ReadJson(arrayObject); + } + } + else + { + if (arrayValue == nullptr) + { + logger_->debug( + "Subcategory array key {} is not present, resetting to " + "defaults", + subcategoryArray.first); + } + else if (!arrayValue->is_object()) + { + logger_->warn( + "Invalid json for subcategory array key {}, resetting to " + "defaults", + p->name_); + } + + for (auto& subcategory : subcategoryArray.second) + { + subcategory->SetDefaults(); + } + validated = false; + } + } + + // Read variables for (auto& variable : p->variables_) { validated &= variable->ReadValue(object); @@ -66,8 +121,8 @@ bool SettingsCategory::ReadJson(const boost::json::object& json) { if (value == nullptr) { - logger_->warn("Key {} is not present, resetting to defaults", - p->name_); + logger_->debug("Key {} is not present, resetting to defaults", + p->name_); } else if (!value->is_object()) { @@ -86,6 +141,20 @@ void SettingsCategory::WriteJson(boost::json::object& json) const { boost::json::object object; + // Write subcategory arrays + for (auto& subcategoryArray : p->subcategoryArrays_) + { + boost::json::object arrayObject; + + for (auto& subcategory : subcategoryArray.second) + { + subcategory->WriteJson(arrayObject); + } + + object.insert_or_assign(subcategoryArray.first, arrayObject); + } + + // Write variables for (auto& variable : p->variables_) { variable->WriteValue(object); @@ -94,6 +163,18 @@ void SettingsCategory::WriteJson(boost::json::object& json) const json.insert_or_assign(p->name_, object); } +void SettingsCategory::RegisterSubcategoryArray( + const std::string& name, std::vector& subcategories) +{ + auto& newSubcategories = p->subcategoryArrays_.emplace_back( + name, std::vector {}); + + std::transform(subcategories.begin(), + subcategories.end(), + std::back_inserter(newSubcategories.second), + [](SettingsCategory& subcategory) { return &subcategory; }); +} + void SettingsCategory::RegisterVariables( std::initializer_list variables) { diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index cc07a7d7..d7c86abd 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -50,7 +50,8 @@ public: */ virtual void WriteJson(boost::json::object& json) const; -protected: + void RegisterSubcategoryArray(const std::string& name, + std::vector& subcategories); void RegisterVariables(std::initializer_list variables); void RegisterVariables(std::vector variables); diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index dd729da0..b7133537 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,16 +27,21 @@ template class SettingsInterface::Impl { public: - explicit Impl() + explicit Impl(SettingsInterface* self) : self_ {self} { context_->moveToThread(QCoreApplication::instance()->thread()); } ~Impl() {} + template + void SetWidgetText(U* widget, const T& currentValue); + void UpdateEditWidget(); void UpdateResetButton(); + SettingsInterface* self_; + SettingsVariable* variable_ {nullptr}; bool stagedValid_ {true}; @@ -49,17 +55,27 @@ public: template SettingsInterface::SettingsInterface() : - SettingsInterfaceBase(), p(std::make_unique()) + SettingsInterfaceBase(), p(std::make_unique(this)) { } template SettingsInterface::~SettingsInterface() = default; template -SettingsInterface::SettingsInterface(SettingsInterface&&) noexcept = default; +SettingsInterface::SettingsInterface(SettingsInterface&& o) noexcept : + p {std::move(o.p)} +{ + p->self_ = this; +} + template SettingsInterface& -SettingsInterface::operator=(SettingsInterface&&) noexcept = default; +SettingsInterface::operator=(SettingsInterface&& o) noexcept +{ + p = std::move(o.p); + p->self_ = this; + return *this; +} template void SettingsInterface::SetSettingsVariable(SettingsVariable& variable) @@ -73,6 +89,27 @@ SettingsVariable* SettingsInterface::GetSettingsVariable() const return p->variable_; } +template +bool SettingsInterface::IsDefault() +{ + bool isDefault = false; + + const std::optional staged = p->variable_->GetStaged(); + const T defaultValue = p->variable_->GetDefault(); + const T value = p->variable_->GetValue(); + + if (staged.has_value()) + { + isDefault = (p->stagedValid_ && *staged == defaultValue); + } + else + { + isDefault = (value == defaultValue); + } + + return isDefault; +} + template bool SettingsInterface::Commit() { @@ -95,6 +132,14 @@ void SettingsInterface::StageDefault() p->UpdateResetButton(); } +template +void SettingsInterface::StageValue(const T& value) +{ + p->variable_->StageValue(value); + p->UpdateEditWidget(); + p->UpdateResetButton(); +} + template void SettingsInterface::SetEditWidget(QWidget* widget) { @@ -105,6 +150,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->editWidget_ = widget; + if (widget == nullptr) + { + return; + } + if (QLineEdit* lineEdit = dynamic_cast(widget)) { if constexpr (std::is_same_v) @@ -274,33 +324,36 @@ void SettingsInterface::SetResetButton(QAbstractButton* button) p->resetButton_ = button; - QObject::connect(p->resetButton_, - &QAbstractButton::clicked, - p->context_.get(), - [this]() - { - T defaultValue = p->variable_->GetDefault(); - - if (p->variable_->GetValue() == defaultValue) + if (p->resetButton_ != nullptr) + { + QObject::connect(p->resetButton_, + &QAbstractButton::clicked, + p->context_.get(), + [this]() { - // If the current value is default, reset the staged - // value - p->variable_->Reset(); - p->stagedValid_ = true; - p->UpdateEditWidget(); - p->UpdateResetButton(); - } - else - { - // Stage the default value - p->stagedValid_ = - p->variable_->StageValue(defaultValue); - p->UpdateEditWidget(); - p->UpdateResetButton(); - } - }); + T defaultValue = p->variable_->GetDefault(); - p->UpdateResetButton(); + if (p->variable_->GetValue() == defaultValue) + { + // If the current value is default, reset the + // staged value + p->variable_->Reset(); + p->stagedValid_ = true; + p->UpdateEditWidget(); + p->UpdateResetButton(); + } + else + { + // Stage the default value + p->stagedValid_ = + p->variable_->StageValue(defaultValue); + p->UpdateEditWidget(); + p->UpdateResetButton(); + } + }); + + p->UpdateResetButton(); + } } template @@ -317,6 +370,39 @@ void SettingsInterface::SetMapToValueFunction( p->mapToValue_ = function; } +template +template +void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) +{ + if constexpr (std::is_integral_v) + { + widget->setText(QString::number(currentValue)); + } + else if constexpr (std::is_same_v) + { + if (mapFromValue_ != nullptr) + { + widget->setText(QString::fromStdString(mapFromValue_(currentValue))); + } + else + { + widget->setText(QString::fromStdString(currentValue)); + } + } + else if constexpr (std::is_same_v>) + { + if (mapFromValue_ != nullptr) + { + widget->setText(QString::fromStdString(mapFromValue_(currentValue))); + } + else + { + widget->setText(QString::fromStdString( + fmt::format("{}", fmt::join(currentValue, ", ")))); + } + } +} + template void SettingsInterface::Impl::UpdateEditWidget() { @@ -327,35 +413,11 @@ void SettingsInterface::Impl::UpdateEditWidget() if (QLineEdit* lineEdit = dynamic_cast(editWidget_)) { - if constexpr (std::is_integral_v) - { - lineEdit->setText(QString::number(currentValue)); - } - else if constexpr (std::is_same_v) - { - if (mapFromValue_ != nullptr) - { - lineEdit->setText( - QString::fromStdString(mapFromValue_(currentValue))); - } - else - { - lineEdit->setText(QString::fromStdString(currentValue)); - } - } - else if constexpr (std::is_same_v>) - { - if (mapFromValue_ != nullptr) - { - lineEdit->setText( - QString::fromStdString(mapFromValue_(currentValue))); - } - else - { - lineEdit->setText(QString::fromStdString( - fmt::format("{}", fmt::join(currentValue, ", ")))); - } - } + SetWidgetText(lineEdit, currentValue); + } + else if (QLabel* label = dynamic_cast(editWidget_)) + { + SetWidgetText(label, currentValue); } else if (QCheckBox* checkBox = dynamic_cast(editWidget_)) { @@ -391,20 +453,9 @@ void SettingsInterface::Impl::UpdateEditWidget() template void SettingsInterface::Impl::UpdateResetButton() { - const std::optional staged = variable_->GetStaged(); - const T defaultValue = variable_->GetDefault(); - const T value = variable_->GetValue(); - if (resetButton_ != nullptr) { - if (staged.has_value()) - { - resetButton_->setVisible(!stagedValid_ || *staged != defaultValue); - } - else - { - resetButton_->setVisible(value != defaultValue); - } + resetButton_->setVisible(!self_->IsDefault()); } } diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 030b8996..f5f5bb4a 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -45,6 +45,14 @@ public: */ SettingsVariable* GetSettingsVariable() const; + /** + * Gets whether the staged value (or current value, if none staged) is + * set to the default value. + * + * @return true if the settings variable is set to default, otherwise false. + */ + bool IsDefault() override; + /** * Sets the current value of the associated settings variable to the staged * value. @@ -64,6 +72,11 @@ public: */ void StageDefault() override; + /** + * Stages a value to the associated settings variable. + */ + void StageValue(const T& value); + /** * Sets the edit widget from the settings dialog. * @@ -103,6 +116,7 @@ private: #ifdef SETTINGS_INTERFACE_IMPLEMENTATION template class SettingsInterface; +template class SettingsInterface; template class SettingsInterface; template class SettingsInterface; diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp index 97ae442d..d0dc2ff2 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp @@ -24,10 +24,18 @@ public: SettingsInterfaceBase(SettingsInterfaceBase&&) noexcept; SettingsInterfaceBase& operator=(SettingsInterfaceBase&&) noexcept; + /** + * Gets whether the staged value (or current value, if none staged) is + * set to the default value. + * + * @return true if the settings variable is set to default, otherwise false. + */ + virtual bool IsDefault() = 0; + /** * Sets the current value of the associated settings variable to the staged * value. - * + * * @return true if the staged value was committed, false if no staged value * is present. */ diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 270a2eed..1f7661c0 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -239,6 +239,12 @@ std::optional SettingsVariable::GetStaged() const return p->staged_; } +template +T SettingsVariable::GetStagedOrValue() const +{ + return p->staged_.value_or(GetValue()); +} + template T SettingsVariable::GetDefault() const { diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index c7999c0d..c05c543f 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -103,6 +103,14 @@ public: */ std::optional GetStaged() const; + /** + * Gets the staged value of the settings variable, if defined, otherwise the + * current value. + * + * @return Staged value or current value + */ + T GetStagedOrValue() const; + /** * Validate the value against the defined parameters of the settings * variable. diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index b96f94f4..fdf41acf 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -12,10 +12,34 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::text_settings"; +static const std::string kAlteDIN1451Mittelscrhift_ { + "Alte DIN 1451 Mittelschrift"}; +static const std::string kInconsolata_ {"Inconsolata"}; + +static const std::string kRegular_ {"Regular"}; + +static const std::unordered_map + kDefaultFontFamily_ { + {types::FontCategory::Default, kAlteDIN1451Mittelscrhift_}, + {types::FontCategory::Tooltip, kInconsolata_}}; +static const std::unordered_map + kDefaultFontStyle_ {{types::FontCategory::Default, kRegular_}, + {types::FontCategory::Tooltip, kRegular_}}; +static const std::unordered_map + kDefaultFontPointSize_ {{types::FontCategory::Default, 12.0}, + {types::FontCategory::Tooltip, 10.5}}; + class TextSettings::Impl { public: - explicit Impl() + struct FontData + { + SettingsVariable fontFamily_ {"font_family"}; + SettingsVariable fontStyle_ {"font_style"}; + SettingsVariable fontPointSize_ {"font_point_size"}; + }; + + explicit Impl(TextSettings* self) : self_ {self} { std::string defaultTooltipMethodValue = types::GetTooltipMethodName(types::TooltipMethod::ImGui); @@ -47,16 +71,32 @@ public: // No match found, invalid return false; }); + + InitializeFontVariables(); } ~Impl() {} + void InitializeFontVariables(); + + friend bool operator==(const FontData& lhs, const FontData& rhs) + { + return (lhs.fontFamily_ == rhs.fontFamily_ && + lhs.fontStyle_ == rhs.fontStyle_ && + lhs.fontPointSize_ == rhs.fontPointSize_); + } + + TextSettings* self_; + + std::unordered_map fontData_ {}; + std::vector fontSettings_ {}; + SettingsVariable hoverTextWrap_ {"hover_text_wrap"}; SettingsVariable tooltipMethod_ {"tooltip_method"}; }; TextSettings::TextSettings() : - SettingsCategory("text"), p(std::make_unique()) + SettingsCategory("text"), p(std::make_unique(this)) { RegisterVariables({&p->hoverTextWrap_, &p->tooltipMethod_}); SetDefaults(); @@ -66,6 +106,57 @@ TextSettings::~TextSettings() = default; TextSettings::TextSettings(TextSettings&&) noexcept = default; TextSettings& TextSettings::operator=(TextSettings&&) noexcept = default; +void TextSettings::Impl::InitializeFontVariables() +{ + for (auto fontCategory : types::FontCategoryIterator()) + { + auto result = fontData_.emplace(fontCategory, FontData {}); + auto& pair = *result.first; + auto& font = pair.second; + + font.fontFamily_.SetDefault(kDefaultFontFamily_.at(fontCategory)); + font.fontStyle_.SetDefault(kDefaultFontStyle_.at(fontCategory)); + font.fontPointSize_.SetDefault(kDefaultFontPointSize_.at(fontCategory)); + + // String values must not be empty + font.fontFamily_.SetValidator([](const std::string& value) + { return !value.empty(); }); + font.fontStyle_.SetValidator([](const std::string& value) + { return !value.empty(); }); + + // Font point size must be between 6 and 72 + font.fontPointSize_.SetMinimum(6.0); + font.fontPointSize_.SetMaximum(72.0); + + // Variable registration + auto& settings = fontSettings_.emplace_back( + SettingsCategory {types::GetFontCategoryName(fontCategory)}); + + settings.RegisterVariables( + {&font.fontFamily_, &font.fontStyle_, &font.fontPointSize_}); + } + + self_->RegisterSubcategoryArray("fonts", fontSettings_); +} + +SettingsVariable& +TextSettings::font_family(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontFamily_; +} + +SettingsVariable& +TextSettings::font_style(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontStyle_; +} + +SettingsVariable& +TextSettings::font_point_size(types::FontCategory fontCategory) const +{ + return p->fontData_.at(fontCategory).fontPointSize_; +} + SettingsVariable& TextSettings::hover_text_wrap() const { return p->hoverTextWrap_; @@ -78,13 +169,14 @@ SettingsVariable& TextSettings::tooltip_method() const TextSettings& TextSettings::Instance() { - static TextSettings TextSettings_; - return TextSettings_; + static TextSettings textSettings_; + return textSettings_; } bool operator==(const TextSettings& lhs, const TextSettings& rhs) { - return (lhs.p->hoverTextWrap_ == rhs.p->hoverTextWrap_ && + return (lhs.p->fontData_ == rhs.p->fontData_ && + lhs.p->hoverTextWrap_ == rhs.p->hoverTextWrap_ && lhs.p->tooltipMethod_ == rhs.p->tooltipMethod_); } diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.hpp b/scwx-qt/source/scwx/qt/settings/text_settings.hpp index 42399f99..a0347b4f 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -25,8 +26,15 @@ public: TextSettings(TextSettings&&) noexcept; TextSettings& operator=(TextSettings&&) noexcept; + SettingsVariable& + font_family(types::FontCategory fontCategory) const; + SettingsVariable& + font_style(types::FontCategory fontCategory) const; + SettingsVariable& + font_point_size(types::FontCategory fontCategory) const; + SettingsVariable& hover_text_wrap() const; - SettingsVariable& tooltip_method() const; + SettingsVariable& tooltip_method() const; static TextSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.cpp b/scwx-qt/source/scwx/qt/types/imgui_font.cpp new file mode 100644 index 00000000..e6f22ad1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/imgui_font.cpp @@ -0,0 +1,83 @@ +// Disable strncpy warning +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::string logPrefix_ = "scwx::qt::types::imgui_font"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class ImGuiFont::Impl +{ +public: + explicit Impl(const std::string& fontName, + const std::vector& fontData, + units::font_size::pixels size) : + fontName_ {fontName}, size_ {size} + { + CreateImGuiFont(fontData); + } + + ~Impl() {} + + void CreateImGuiFont(const std::vector& fontData); + + const std::string fontName_; + const units::font_size::pixels size_; + + ImFont* imFont_ {nullptr}; +}; + +ImGuiFont::ImGuiFont(const std::string& fontName, + const std::vector& fontData, + units::font_size::pixels size) : + p(std::make_unique(fontName, fontData, size)) +{ +} +ImGuiFont::~ImGuiFont() = default; + +void ImGuiFont::Impl::CreateImGuiFont(const std::vector& fontData) +{ + logger_->debug("Creating Font: {}", fontName_); + + ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); + ImFontConfig fontConfig {}; + + const float sizePixels = static_cast(size_.value()); + + // Do not transfer ownership of font data to ImGui, makes const_cast safe + fontConfig.FontDataOwnedByAtlas = false; + + // Assign name to font + strncpy(fontConfig.Name, fontName_.c_str(), sizeof(fontConfig.Name) - 1); + fontConfig.Name[sizeof(fontConfig.Name) - 1] = 0; + + imFont_ = fontAtlas->AddFontFromMemoryTTF( + const_cast(static_cast(fontData.data())), + static_cast(std::clamp( + fontData.size(), 0, std::numeric_limits::max())), + sizePixels, + &fontConfig); +} + +ImFont* ImGuiFont::font() +{ + return p->imFont_; +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.hpp b/scwx-qt/source/scwx/qt/types/imgui_font.hpp new file mode 100644 index 00000000..ace8ba09 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/imgui_font.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include + +struct ImFont; + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +class ImGuiFont +{ +public: + explicit ImGuiFont(const std::string& fontName, + const std::vector& fontData, + units::font_size::pixels size); + ~ImGuiFont(); + + ImGuiFont(const ImGuiFont&) = delete; + ImGuiFont& operator=(const ImGuiFont&) = delete; + + ImGuiFont(ImGuiFont&&) = delete; + ImGuiFont& operator=(ImGuiFont&&) = delete; + + ImFont* font(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/text_types.cpp b/scwx-qt/source/scwx/qt/types/text_types.cpp index e708cf63..f994dab9 100644 --- a/scwx-qt/source/scwx/qt/types/text_types.cpp +++ b/scwx-qt/source/scwx/qt/types/text_types.cpp @@ -11,12 +11,40 @@ namespace qt namespace types { +static const std::unordered_map fontCategoryName_ { + {FontCategory::Default, "Default"}, + {FontCategory::Tooltip, "Tooltip"}, + {FontCategory::Unknown, "?"}}; + static const std::unordered_map tooltipMethodName_ { {TooltipMethod::ImGui, "ImGui"}, {TooltipMethod::QToolTip, "Native Tooltip"}, {TooltipMethod::QLabel, "Floating Label"}, {TooltipMethod::Unknown, "?"}}; +FontCategory GetFontCategory(const std::string& name) +{ + auto result = + std::find_if(fontCategoryName_.cbegin(), + fontCategoryName_.cend(), + [&](const std::pair& pair) -> bool + { return boost::iequals(pair.second, name); }); + + if (result != fontCategoryName_.cend()) + { + return result->first; + } + else + { + return FontCategory::Unknown; + } +} + +std::string GetFontCategoryName(FontCategory fontCategory) +{ + return fontCategoryName_.at(fontCategory); +} + TooltipMethod GetTooltipMethod(const std::string& name) { auto result = std::find_if( diff --git a/scwx-qt/source/scwx/qt/types/text_types.hpp b/scwx-qt/source/scwx/qt/types/text_types.hpp index 98025a86..07c1ea00 100644 --- a/scwx-qt/source/scwx/qt/types/text_types.hpp +++ b/scwx-qt/source/scwx/qt/types/text_types.hpp @@ -11,6 +11,16 @@ namespace qt namespace types { +enum class FontCategory +{ + Default, + Tooltip, + Unknown +}; +typedef scwx::util:: + Iterator + FontCategoryIterator; + enum class TooltipMethod { ImGui, @@ -22,6 +32,8 @@ typedef scwx::util:: Iterator TooltipMethodIterator; +FontCategory GetFontCategory(const std::string& name); +std::string GetFontCategoryName(FontCategory fontCategory); TooltipMethod GetTooltipMethod(const std::string& name); std::string GetTooltipMethodName(TooltipMethod tooltipMethod); diff --git a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp index a42be06f..42ec4e32 100644 --- a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp @@ -1,7 +1,7 @@ #include "about_dialog.hpp" #include "ui_about_dialog.h" #include -#include +#include #include @@ -27,7 +27,7 @@ AboutDialog::AboutDialog(QWidget* parent) : ui->setupUi(this); int titleFontId = - manager::ResourceManager::FontId(types::Font::din1451alt_g); + manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); QFont titleFont(titleFontFamily, 14); diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index 5efca78a..e827469c 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -1,10 +1,10 @@ #include "alert_dock_widget.hpp" #include "ui_alert_dock_widget.h" -#include #include #include #include +#include #include #include #include @@ -175,10 +175,10 @@ void AlertDockWidgetImpl::ConnectSignals() // If an item is selected if (selectedAlertKey_ != types::TextEventKey {}) { - types::AlertAction alertAction = types::GetAlertAction( - manager::SettingsManager::general_settings() - .default_alert_action() - .GetValue()); + types::AlertAction alertAction = + types::GetAlertAction(settings::GeneralSettings::Instance() + .default_alert_action() + .GetValue()); switch (alertAction) { diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 34b1001a..f19be01f 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -1,7 +1,7 @@ #include "animation_dock_widget.hpp" #include "ui_animation_dock_widget.h" -#include +#include #include #include @@ -101,7 +101,7 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : maxDateTimer->start(15000); // Set loop defaults - auto& generalSettings = manager::SettingsManager::general_settings(); + auto& generalSettings = settings::GeneralSettings::Instance(); ui->loopTimeSpinBox->setValue(generalSettings.loop_time().GetValue()); ui->loopSpeedSpinBox->setValue(generalSettings.loop_speed().GetValue()); ui->loopDelaySpinBox->setValue(generalSettings.loop_delay().GetValue() * @@ -175,7 +175,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](int i) { - manager::SettingsManager::general_settings().loop_time().StageValue(i); + settings::GeneralSettings::Instance().loop_time().StageValue(i); Q_EMIT self_->LoopTimeChanged(std::chrono::minutes(i)); }); QObject::connect( @@ -184,8 +184,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](double d) { - manager::SettingsManager::general_settings().loop_speed().StageValue( - d); + settings::GeneralSettings::Instance().loop_speed().StageValue(d); Q_EMIT self_->LoopSpeedChanged(d); }); QObject::connect( @@ -194,7 +193,7 @@ void AnimationDockWidgetImpl::ConnectSignals() self_, [this](double d) { - manager::SettingsManager::general_settings().loop_delay().StageValue( + settings::GeneralSettings::Instance().loop_delay().StageValue( static_cast(d * 1000.0)); Q_EMIT self_->LoopDelayChanged(std::chrono::milliseconds( static_cast(d * 1000.0))); diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp index 18011b7f..fcbed5d6 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -50,6 +51,8 @@ public: model::ImGuiContextModel::Instance().DestroyContext(contextName_); } + void ImGuiCheckFonts(); + ImGuiDebugWidget* self_; ImGuiContext* context_; std::string contextName_; @@ -58,6 +61,7 @@ public: std::set renderedSet_ {}; bool imGuiRendererInitialized_ {false}; + std::uint64_t imGuiFontsBuildCount_ {}; }; ImGuiDebugWidget::ImGuiDebugWidget(QWidget* parent) : @@ -102,6 +106,8 @@ void ImGuiDebugWidget::initializeGL() // Initialize ImGui OpenGL3 backend ImGui::SetCurrentContext(p->context_); ImGui_ImplOpenGL3_Init(); + p->imGuiFontsBuildCount_ = + manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; } @@ -109,9 +115,13 @@ void ImGuiDebugWidget::paintGL() { ImGui::SetCurrentContext(p->currentContext_); + // Lock ImGui font atlas prior to new ImGui frame + std::shared_lock imguiFontAtlasLock { + manager::FontManager::Instance().imgui_font_atlas_mutex()}; + ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); - + p->ImGuiCheckFonts(); ImGui::NewFrame(); if (!p->renderedSet_.contains(p->currentContext_)) @@ -131,6 +141,29 @@ void ImGuiDebugWidget::paintGL() ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + // Unlock ImGui font atlas after rendering + imguiFontAtlasLock.unlock(); +} + +void ImGuiDebugWidgetImpl::ImGuiCheckFonts() +{ + // Update ImGui Fonts if required + std::uint64_t currentImGuiFontsBuildCount = + manager::FontManager::Instance().imgui_fonts_build_count(); + + if ((context_ == currentContext_ && + imGuiFontsBuildCount_ != currentImGuiFontsBuildCount) || + !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + { + ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplOpenGL3_CreateFontsTexture(); + } + + if (context_ == currentContext_) + { + imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; + } } } // namespace ui diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 9bb52e00..9e7310a2 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -6,9 +6,12 @@ #include #include #include +#include +#include #include #include #include +#include #include #include #include @@ -21,6 +24,8 @@ #include #include #include +#include +#include #include namespace scwx @@ -84,9 +89,10 @@ public: explicit SettingsDialogImpl(SettingsDialog* self) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, + fontDialog_ {new QFontDialog(self)}, + fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { &defaultRadarSite_, - &fontSizes_, &gridWidth_, &gridHeight_, &mapProvider_, @@ -99,7 +105,7 @@ public: &tooltipMethod_}} { // Configure default alert phenomena colors - auto& paletteSettings = manager::SettingsManager::palette_settings(); + auto& paletteSettings = settings::PaletteSettings::Instance(); int index = 0; for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) @@ -113,6 +119,12 @@ public: QColor(QString::fromStdString( paletteSettings.alert_color(phenomenon, false).GetDefault()))); } + + // Configure font dialog + fontDialog_->setOptions( + QFontDialog::FontDialogOption::DontUseNativeDialog | + QFontDialog::FontDialogOption::ScalableFonts); + fontDialog_->setWindowModality(Qt::WindowModality::WindowModal); } ~SettingsDialogImpl() = default; @@ -126,6 +138,10 @@ public: void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); void UpdateRadarDialogLocation(const std::string& id); + QFont GetSelectedFont(); + void SelectFontCategory(types::FontCategory fontCategory); + void UpdateFontDisplayData(); + void ApplyChanges(); void DiscardChanges(); void ResetToDefault(); @@ -146,17 +162,21 @@ public: SettingsDialog* self_; PlacefileSettingsWidget* placefileSettingsWidget_ {nullptr}; RadarSiteDialog* radarSiteDialog_; + QFontDialog* fontDialog_; - settings::SettingsInterface defaultRadarSite_ {}; - settings::SettingsInterface> fontSizes_ {}; - settings::SettingsInterface gridWidth_ {}; - settings::SettingsInterface gridHeight_ {}; - settings::SettingsInterface mapProvider_ {}; - settings::SettingsInterface mapboxApiKey_ {}; - settings::SettingsInterface mapTilerApiKey_ {}; - settings::SettingsInterface defaultAlertAction_ {}; - settings::SettingsInterface updateNotificationsEnabled_ {}; - settings::SettingsInterface debugEnabled_ {}; + QStandardItemModel* fontCategoryModel_; + + types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; + + settings::SettingsInterface defaultRadarSite_ {}; + settings::SettingsInterface gridWidth_ {}; + settings::SettingsInterface gridHeight_ {}; + settings::SettingsInterface mapProvider_ {}; + settings::SettingsInterface mapboxApiKey_ {}; + settings::SettingsInterface mapTilerApiKey_ {}; + settings::SettingsInterface defaultAlertAction_ {}; + settings::SettingsInterface updateNotificationsEnabled_ {}; + settings::SettingsInterface debugEnabled_ {}; std::unordered_map> colorTables_ {}; @@ -167,6 +187,15 @@ public: settings::SettingsInterface> inactiveAlertColors_ {}; + std::unordered_map> + fontFamilies_ {}; + std::unordered_map> + fontStyles_ {}; + std::unordered_map> + fontPointSizes_ {}; + settings::SettingsInterface hoverTextWrap_ {}; settings::SettingsInterface tooltipMethod_ {}; @@ -239,6 +268,72 @@ void SettingsDialogImpl::ConnectSignals() [this](const std::string& newValue) { UpdateRadarDialogLocation(newValue); }); + QObject::connect( + self_->ui->fontListView->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [this](const QItemSelection& selected, const QItemSelection& deselected) + { + if (selected.size() == 0 && deselected.size() == 0) + { + // Items which stay selected but change their index are not + // included in selected and deselected. Thus, this signal might + // be emitted with both selected and deselected empty, if only + // the indices of selected items change. + return; + } + + if (selected.size() > 0) + { + QModelIndex selectedIndex = selected[0].indexes()[0]; + QVariant variantData = + self_->ui->fontListView->model()->data(selectedIndex); + if (variantData.typeId() == QMetaType::QString) + { + types::FontCategory fontCategory = + types::GetFontCategory(variantData.toString().toStdString()); + SelectFontCategory(fontCategory); + UpdateFontDisplayData(); + } + } + }); + + QObject::connect(self_->ui->fontSelectButton, + &QAbstractButton::clicked, + self_, + [this]() + { + fontDialog_->setCurrentFont(GetSelectedFont()); + fontDialog_->show(); + }); + + QObject::connect(fontDialog_, + &QFontDialog::fontSelected, + self_, + [this](const QFont& font) + { + fontFamilies_.at(selectedFontCategory_) + .StageValue(font.family().toStdString()); + fontStyles_.at(selectedFontCategory_) + .StageValue(font.styleName().toStdString()); + fontPointSizes_.at(selectedFontCategory_) + .StageValue(font.pointSizeF()); + + UpdateFontDisplayData(); + }); + + QObject::connect(self_->ui->resetFontButton, + &QAbstractButton::clicked, + self_, + [this]() + { + fontFamilies_.at(selectedFontCategory_).StageDefault(); + fontStyles_.at(selectedFontCategory_).StageDefault(); + fontPointSizes_.at(selectedFontCategory_).StageDefault(); + + UpdateFontDisplayData(); + }); + QObject::connect( self_->ui->buttonBox, &QDialogButtonBox::clicked, @@ -289,7 +384,7 @@ void SettingsDialogImpl::SetupGeneralTab() } settings::GeneralSettings& generalSettings = - manager::SettingsManager::general_settings(); + settings::GeneralSettings::Instance(); defaultRadarSite_.SetSettingsVariable(generalSettings.default_radar_site()); defaultRadarSite_.SetMapFromValueFunction( @@ -327,10 +422,6 @@ void SettingsDialogImpl::SetupGeneralTab() 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); @@ -430,7 +521,7 @@ void SettingsDialogImpl::SetupGeneralTab() void SettingsDialogImpl::SetupPalettesColorTablesTab() { settings::PaletteSettings& paletteSettings = - manager::SettingsManager::palette_settings(); + settings::PaletteSettings::Instance(); // Palettes > Color Tables QGridLayout* colorTableLayout = @@ -522,7 +613,7 @@ void SettingsDialogImpl::SetupPalettesColorTablesTab() void SettingsDialogImpl::SetupPalettesAlertsTab() { settings::PaletteSettings& paletteSettings = - manager::SettingsManager::palette_settings(); + settings::PaletteSettings::Instance(); // Palettes > Alerts QGridLayout* alertsLayout = @@ -645,6 +736,39 @@ void SettingsDialogImpl::SetupTextTab() { settings::TextSettings& textSettings = settings::TextSettings::Instance(); + self_->ui->fontListView->setModel(fontCategoryModel_); + for (const auto& fontCategory : types::FontCategoryIterator()) + { + // Add font category to list view + fontCategoryModel_->appendRow(new QStandardItem( + QString::fromStdString(types::GetFontCategoryName(fontCategory)))); + + // Create settings interface + auto fontFamilyResult = fontFamilies_.emplace( + fontCategory, settings::SettingsInterface {}); + auto fontStyleResult = fontStyles_.emplace( + fontCategory, settings::SettingsInterface {}); + auto fontSizeResult = fontPointSizes_.emplace( + fontCategory, settings::SettingsInterface {}); + + auto& fontFamily = (*fontFamilyResult.first).second; + auto& fontStyle = (*fontStyleResult.first).second; + auto& fontSize = (*fontSizeResult.first).second; + + // Add to settings list + settings_.push_back(&fontFamily); + settings_.push_back(&fontStyle); + settings_.push_back(&fontSize); + + // Set settings variables + fontFamily.SetSettingsVariable(textSettings.font_family(fontCategory)); + fontStyle.SetSettingsVariable(textSettings.font_style(fontCategory)); + fontSize.SetSettingsVariable(textSettings.font_point_size(fontCategory)); + } + self_->ui->fontListView->setCurrentIndex(fontCategoryModel_->index(0, 0)); + SelectFontCategory(*types::FontCategoryIterator().begin()); + UpdateFontDisplayData(); + hoverTextWrap_.SetSettingsVariable(textSettings.hover_text_wrap()); hoverTextWrap_.SetEditWidget(self_->ui->hoverTextWrapSpinBox); hoverTextWrap_.SetResetButton(self_->ui->resetHoverTextWrapButton); @@ -799,6 +923,53 @@ void SettingsDialogImpl::UpdateRadarDialogLocation(const std::string& id) } } +QFont SettingsDialogImpl::GetSelectedFont() +{ + std::string fontFamily = fontFamilies_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue(); + std::string fontStyle = fontStyles_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue(); + units::font_size::points fontSize { + fontPointSizes_.at(selectedFontCategory_) + .GetSettingsVariable() + ->GetStagedOrValue()}; + + QFont font(QString::fromStdString(fontFamily)); + font.setStyleName(QString::fromStdString(fontStyle)); + font.setPointSizeF(fontSize.value()); + + return font; +} + +void SettingsDialogImpl::SelectFontCategory(types::FontCategory fontCategory) +{ + selectedFontCategory_ = fontCategory; +} + +void SettingsDialogImpl::UpdateFontDisplayData() +{ + QFont font = GetSelectedFont(); + + self_->ui->fontNameLabel->setText(font.family()); + self_->ui->fontStyleLabel->setText(font.styleName()); + self_->ui->fontSizeLabel->setText(QString::number(font.pointSizeF())); + + self_->ui->fontPreviewLabel->setFont(font); + + if (selectedFontCategory_ != types::FontCategory::Unknown) + { + auto& fontFamily = fontFamilies_.at(selectedFontCategory_); + auto& fontStyle = fontStyles_.at(selectedFontCategory_); + auto& fontSize = fontPointSizes_.at(selectedFontCategory_); + + self_->ui->resetFontButton->setVisible(!fontFamily.IsDefault() || + !fontStyle.IsDefault() || + !fontSize.IsDefault()); + } +} + void SettingsDialogImpl::ApplyChanges() { logger_->info("Applying settings changes"); @@ -812,7 +983,7 @@ void SettingsDialogImpl::ApplyChanges() if (committed) { - manager::SettingsManager::SaveSettings(); + manager::SettingsManager::Instance().SaveSettings(); } } diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 15d5d1ec..787c124e 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -127,27 +127,7 @@ 0 - - - - - - - Font Sizes - - - - - - - - - - Default Radar Site - - - - + ... @@ -158,45 +138,14 @@ - - - - - + + - Mapbox API Key + Grid Width - - - - MapTiler API Key - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - + ... @@ -207,16 +156,6 @@ - - - - QLineEdit::Password - - - - - - @@ -228,32 +167,14 @@ - - + + - Grid Height + MapTiler API Key - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - QLineEdit::Password - - - - + ... @@ -264,21 +185,97 @@ + + + + + + - + - Grid Width + Grid Height + + + + + + + Default Radar Site + + + + + + + QLineEdit::Password + + + + + + + QLineEdit::Password + + + + + + + Default Alert Action + + + Mapbox API Key + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + ... + + + + Map Provider - + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... @@ -289,26 +286,8 @@ - - - - Default Alert Action - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - + + @@ -438,6 +417,194 @@ + + + + QFrame::StyledPanel + + + QFrame::Plain + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Display Item: + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QFrame::Panel + + + QFrame::Plain + + + + + + Tornado Warning expires in 15 minutes + + + Qt::AlignCenter + + + true + + + + + + + + + + ... + + + + + + + [Style] + + + + + + + [Font Name] + + + + + + + [Size] + + + + + + + Font: + + + + + + + Style: + + + + + + + Size: + + + + + + + Preview: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 93b3ac0a..4029fa9a 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -1,7 +1,7 @@ #include "update_dialog.hpp" #include "ui_update_dialog.h" #include -#include +#include #include #include @@ -30,7 +30,7 @@ UpdateDialog::UpdateDialog(QWidget* parent) : ui->setupUi(this); int titleFontId = - manager::ResourceManager::FontId(types::Font::din1451alt_g); + manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); QFont titleFont(titleFontFamily, 12); diff --git a/scwx-qt/source/scwx/qt/util/font.cpp b/scwx-qt/source/scwx/qt/util/font.cpp index 49fdee3a..f2397bd2 100644 --- a/scwx-qt/source/scwx/qt/util/font.cpp +++ b/scwx-qt/source/scwx/qt/util/font.cpp @@ -5,8 +5,8 @@ #define _CRT_SECURE_NO_WARNINGS #include -#include #include +#include #include #include @@ -126,9 +126,6 @@ public: } } - void CreateImGuiFont(QFile& fontFile, - QByteArray& fontData, - const std::vector& fontSizes); void ParseNames(FT_Face face); const std::string resource_; @@ -266,39 +263,6 @@ GLuint Font::GenerateTexture(gl::OpenGLFunctions& gl) return p->atlas_->id; } -void FontImpl::CreateImGuiFont(QFile& fontFile, - QByteArray& fontData, - const std::vector& fontSizes) -{ - QFileInfo fileInfo(fontFile); - ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); - ImFontConfig fontConfig {}; - - // Do not transfer ownership of font data to ImGui, makes const_cast safe - fontConfig.FontDataOwnedByAtlas = false; - - for (int64_t fontSize : fontSizes) - { - const float sizePixels = static_cast(fontSize); - - // Assign name to font - strncpy(fontConfig.Name, - fmt::format("{}:{}", fileInfo.fileName().toStdString(), fontSize) - .c_str(), - sizeof(fontConfig.Name) - 1); - fontConfig.Name[sizeof(fontConfig.Name) - 1] = 0; - - // Add font to atlas - imGuiFonts_.emplace( - fontSize, - fontAtlas->AddFontFromMemoryTTF( - const_cast(static_cast(fontData.constData())), - fontData.size(), - sizePixels, - &fontConfig)); - } -} - ImFont* Font::ImGuiFont(std::size_t fontPixelSize) { auto it = p->imGuiFonts_.find(fontPixelSize); @@ -334,11 +298,6 @@ std::shared_ptr Font::Create(const std::string& resource) font = std::make_shared(resource); QByteArray fontData = fontFile.readAll(); - font->p->CreateImGuiFont( - fontFile, - fontData, - manager::SettingsManager::general_settings().font_sizes().GetValue()); - font->p->atlas_ = ftgl::texture_atlas_new(512, 512, 1); ftgl::texture_font_t* textureFont = ftgl::texture_font_new_from_memory( font->p->atlas_, BASE_POINT_SIZE, fontData.constData(), fontData.size()); diff --git a/scwx-qt/source/scwx/qt/util/imgui.cpp b/scwx-qt/source/scwx/qt/util/imgui.cpp index 9d1622ed..46bc859e 100644 --- a/scwx-qt/source/scwx/qt/util/imgui.cpp +++ b/scwx-qt/source/scwx/qt/util/imgui.cpp @@ -1,10 +1,7 @@ #include -#include -#include +#include #include -#include - #include namespace scwx @@ -22,13 +19,6 @@ class ImGui::Impl public: explicit Impl() {} ~Impl() {} - - void Initialize(); - void UpdateMonospaceFont(); - - bool initialized_ {false}; - - ImFont* monospaceFont_ {nullptr}; }; ImGui::ImGui() : p(std::make_unique()) {} @@ -37,58 +27,13 @@ ImGui::~ImGui() = default; ImGui::ImGui(ImGui&&) noexcept = default; ImGui& ImGui::operator=(ImGui&&) noexcept = default; -void ImGui::Impl::Initialize() -{ - if (initialized_) - { - return; - } - - logger_->debug("Initialize"); - - // Configure monospace font - UpdateMonospaceFont(); - manager::SettingsManager::general_settings() - .font_sizes() - .RegisterValueChangedCallback([this](const std::vector&) - { UpdateMonospaceFont(); }); - - initialized_ = true; -} - -void ImGui::Impl::UpdateMonospaceFont() -{ - // Get monospace font size - std::size_t fontSize = 16; - auto fontSizes = - manager::SettingsManager::general_settings().font_sizes().GetValue(); - if (fontSizes.size() > 1) - { - fontSize = fontSizes[1]; - } - else if (fontSizes.size() > 0) - { - fontSize = fontSizes[0]; - } - - // Get monospace font pointer - auto monospace = - manager::ResourceManager::Font(types::Font::Inconsolata_Regular); - auto monospaceFont = monospace->ImGuiFont(fontSize); - - // Store monospace font pointer if not null - if (monospaceFont != nullptr) - { - monospaceFont_ = monospace->ImGuiFont(fontSize); - } -} - void ImGui::DrawTooltip(const std::string& hoverText) { - p->Initialize(); + auto tooltipFont = manager::FontManager::Instance().GetImGuiFont( + types::FontCategory::Tooltip); ::ImGui::BeginTooltip(); - ::ImGui::PushFont(p->monospaceFont_); + ::ImGui::PushFont(tooltipFont->font()); ::ImGui::TextUnformatted(hoverText.c_str()); ::ImGui::PopFont(); ::ImGui::EndTooltip(); diff --git a/scwx-qt/source/scwx/qt/util/tooltip.cpp b/scwx-qt/source/scwx/qt/util/tooltip.cpp index 5d7b4c6c..509a0495 100644 --- a/scwx-qt/source/scwx/qt/util/tooltip.cpp +++ b/scwx-qt/source/scwx/qt/util/tooltip.cpp @@ -1,8 +1,6 @@ #include -#include +#include #include -#include -#include #include #include @@ -84,12 +82,22 @@ void Show(const std::string& text, const QPointF& mouseGlobalPos) } else if (tooltipMethod == types::TooltipMethod::QToolTip) { + QString fontFamily = QString::fromStdString( + textSettings.font_family(types::FontCategory::Tooltip).GetValue()); + QString fontStyle = QString::fromStdString( + textSettings.font_style(types::FontCategory::Tooltip).GetValue()); + double fontPointSize = + textSettings.font_point_size(types::FontCategory::Tooltip).GetValue(); + static std::size_t id = 0; QToolTip::showText( mouseGlobalPos.toPoint(), - QString("%3") + QString("%5") .arg(++id) - .arg("Inconsolata") + .arg(fontFamily) + .arg(fontStyle) + .arg(fontPointSize) .arg(QString::fromStdString(displayText).replace("\n", "
")), tooltipParent_.get(), {}, @@ -97,22 +105,9 @@ void Show(const std::string& text, const QPointF& mouseGlobalPos) } else if (tooltipMethod == types::TooltipMethod::QLabel) { - // Get monospace font size - units::font_size::pixels fontSize {16}; - auto fontSizes = - manager::SettingsManager::general_settings().font_sizes().GetValue(); - if (fontSizes.size() > 1) - { - fontSize = units::font_size::pixels {fontSizes[1]}; - } - else if (fontSizes.size() > 0) - { - fontSize = units::font_size::pixels {fontSizes[0]}; - } - // Configure the label - QFont font("Inconsolata"); - font.setPointSizeF(units::font_size::points(fontSize).value()); + QFont font = manager::FontManager::Instance().GetQFont( + types::FontCategory::Tooltip); tooltipLabel_->setFont(font); tooltipLabel_->setText(QString::fromStdString(displayText)); diff --git a/test/data b/test/data index 33caca18..1685e404 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 33caca188b1007c643db75afa560fdfe348c0ee5 +Subproject commit 1685e4048ef4a9f34bc11ecbb8db4905dd0a2e19 diff --git a/test/source/scwx/qt/manager/settings_manager.test.cpp b/test/source/scwx/qt/manager/settings_manager.test.cpp index be576069..54df04df 100644 --- a/test/source/scwx/qt/manager/settings_manager.test.cpp +++ b/test/source/scwx/qt/manager/settings_manager.test.cpp @@ -1,5 +1,10 @@ #include #include +#include +#include +#include +#include +#include #include #include @@ -39,10 +44,14 @@ void VerifyDefaults() settings::GeneralSettings defaultGeneralSettings {}; settings::MapSettings defaultMapSettings {}; settings::PaletteSettings defaultPaletteSettings {}; + settings::TextSettings defaultTextSettings {}; + settings::UiSettings defaultUiSettings {}; - EXPECT_EQ(defaultGeneralSettings, SettingsManager::general_settings()); - EXPECT_EQ(defaultMapSettings, SettingsManager::map_settings()); - EXPECT_EQ(defaultPaletteSettings, SettingsManager::palette_settings()); + EXPECT_EQ(defaultGeneralSettings, settings::GeneralSettings::Instance()); + EXPECT_EQ(defaultMapSettings, settings::MapSettings::Instance()); + EXPECT_EQ(defaultPaletteSettings, settings::PaletteSettings::Instance()); + EXPECT_EQ(defaultTextSettings, settings::TextSettings::Instance()); + EXPECT_EQ(defaultUiSettings, settings::UiSettings::Instance()); } void CompareFiles(const std::string& file1, const std::string& file2) @@ -67,7 +76,7 @@ TEST_F(SettingsManagerTest, CreateJson) // Verify file doesn't exist prior to test start EXPECT_EQ(std::filesystem::exists(filename), false); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); EXPECT_EQ(std::filesystem::exists(filename), true); @@ -83,14 +92,14 @@ TEST_F(SettingsManagerTest, SettingsKeax) std::string filename(std::string(SCWX_TEST_DATA_DIR) + "/json/settings/settings-keax.json"); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); EXPECT_EQ( - SettingsManager::general_settings().default_radar_site().GetValue(), + settings::GeneralSettings::Instance().default_radar_site().GetValue(), "KEAX"); - for (size_t i = 0; i < SettingsManager::map_settings().count(); ++i) + for (size_t i = 0; i < settings::MapSettings::Instance().count(); ++i) { - EXPECT_EQ(SettingsManager::map_settings().radar_site(i).GetValue(), + EXPECT_EQ(settings::MapSettings::Instance().radar_site(i).GetValue(), "KEAX"); } } @@ -103,7 +112,7 @@ TEST_P(DefaultSettingsTest, DefaultSettings) std::filesystem::copy_file(sourceFile, filename); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); VerifyDefaults(); CompareFiles(filename, DEFAULT_SETTINGS_FILE); @@ -131,7 +140,7 @@ TEST_P(BadSettingsTest, BadSettings) std::filesystem::copy_file(sourceFile, filename); - SettingsManager::ReadSettings(filename); + SettingsManager::Instance().ReadSettings(filename); CompareFiles(filename, goodFile); diff --git a/wxdata/include/scwx/gr/placefile.hpp b/wxdata/include/scwx/gr/placefile.hpp index 017cc112..e9b381fe 100644 --- a/wxdata/include/scwx/gr/placefile.hpp +++ b/wxdata/include/scwx/gr/placefile.hpp @@ -67,6 +67,9 @@ public: std::size_t pixels_ {}; std::int32_t flags_ {}; std::string face_ {}; + + bool IsBold() { return flags_ & 1; } + bool IsItalic() { return flags_ & 2; } }; struct DrawItem