diff --git a/.clang-tidy b/.clang-tidy index 602e3d0c..3c98e81d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,4 +10,6 @@ Checks: - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-modernize-use-trailing-return-type' + - '-bugprone-easily-swappable-parameters' + - '-modernize-return-braced-init-list' FormatStyle: 'file' diff --git a/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg new file mode 100644 index 00000000..b16bc330 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg new file mode 100644 index 00000000..cf0df19a --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/building-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-solid.svg new file mode 100644 index 00000000..6f6d3f24 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/building-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg new file mode 100644 index 00000000..c341214f --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg new file mode 100644 index 00000000..59f65e1e --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg new file mode 100644 index 00000000..5bb1ea5c --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/location-pin.svg b/scwx-qt/res/icons/font-awesome-6/location-pin.svg new file mode 100644 index 00000000..4b6182cd --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/location-pin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg new file mode 100644 index 00000000..41bcd103 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/tent-solid.svg b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg new file mode 100644 index 00000000..9f159d60 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/textures/images/location-marker.svg b/scwx-qt/res/textures/images/location-marker.svg index 8ebb064f..3eef9d9e 100644 --- a/scwx-qt/res/textures/images/location-marker.svg +++ b/scwx-qt/res/textures/images/location-marker.svg @@ -6,6 +6,6 @@ + stroke="#ffffff" stroke-width="20" fill="none"/> diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1c654b0e..706ac038 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -222,8 +222,8 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/layer_types.hpp source/scwx/qt/types/location_types.hpp source/scwx/qt/types/map_types.hpp - source/scwx/qt/types/media_types.hpp source/scwx/qt/types/marker_types.hpp + source/scwx/qt/types/media_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp source/scwx/qt/types/text_event_key.hpp @@ -255,6 +255,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/county_dialog.hpp source/scwx/qt/ui/download_dialog.hpp source/scwx/qt/ui/edit_line_dialog.hpp + source/scwx/qt/ui/edit_marker_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/gps_info_dialog.hpp source/scwx/qt/ui/hotkey_edit.hpp @@ -285,6 +286,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/county_dialog.cpp source/scwx/qt/ui/download_dialog.cpp source/scwx/qt/ui/edit_line_dialog.cpp + source/scwx/qt/ui/edit_marker_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/gps_info_dialog.cpp source/scwx/qt/ui/hotkey_edit.cpp @@ -314,6 +316,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/collapsible_group.ui source/scwx/qt/ui/county_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui + source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui @@ -357,6 +360,7 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/network.hpp source/scwx/qt/util/streams.hpp source/scwx/qt/util/texture_atlas.hpp + source/scwx/qt/util/q_color_modulate.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp source/scwx/qt/util/time.hpp @@ -369,6 +373,7 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/maplibre.cpp source/scwx/qt/util/network.cpp source/scwx/qt/util/texture_atlas.cpp + source/scwx/qt/util/q_color_modulate.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp source/scwx/qt/util/time.cpp diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index cca0f62c..e7d8315a 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -32,6 +32,10 @@ res/icons/font-awesome-6/angles-up-solid.svg res/icons/font-awesome-6/backward-step-solid.svg res/icons/font-awesome-6/book-solid.svg + res/icons/font-awesome-6/briefcase-solid.svg + res/icons/font-awesome-6/building-columns-solid.svg + res/icons/font-awesome-6/building-solid.svg + res/icons/font-awesome-6/caravan-solid.svg res/icons/font-awesome-6/copy-regular.svg res/icons/font-awesome-6/discord.svg res/icons/font-awesome-6/earth-americas-solid.svg @@ -40,8 +44,11 @@ res/icons/font-awesome-6/gears-solid.svg res/icons/font-awesome-6/github.svg res/icons/font-awesome-6/house-solid.svg + res/icons/font-awesome-6/house-solid-white.svg res/icons/font-awesome-6/keyboard-regular.svg res/icons/font-awesome-6/layer-group-solid.svg + res/icons/font-awesome-6/location-crosshairs-solid.svg + res/icons/font-awesome-6/location-pin.svg res/icons/font-awesome-6/palette-solid.svg res/icons/font-awesome-6/pause-solid.svg res/icons/font-awesome-6/play-solid.svg @@ -53,7 +60,9 @@ res/icons/font-awesome-6/square-minus-regular.svg res/icons/font-awesome-6/square-plus-regular.svg res/icons/font-awesome-6/star-solid.svg + res/icons/font-awesome-6/star-solid-white.svg res/icons/font-awesome-6/stop-solid.svg + res/icons/font-awesome-6/tent-solid.svg res/icons/font-awesome-6/volume-high-solid.svg res/palettes/wct/CC.pal res/palettes/wct/Default16.pal diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 9e2a8d17..743b0df9 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -38,7 +38,7 @@ static constexpr std::size_t kIntegersPerVertex_ = 4; static constexpr std::size_t kIntegerBufferLength_ = kNumTriangles * kVerticesPerTriangle * kIntegersPerVertex_; -struct GeoIconDrawItem +struct GeoIconDrawItem : types::EventHandler { units::length::nautical_miles threshold_ {}; std::chrono::sys_time startTime_ {}; @@ -691,7 +691,7 @@ void GeoIcons::Impl::UpdateSingleBuffer( hoverIcons.end(), [&di](auto& entry) { return entry.di_ == di; }); - if (di->visible_ && !di->hoverText_.empty()) + if (di->visible_ && (!di->hoverText_.empty() || di->event_ != nullptr)) { const units::angle::radians radians = angle; @@ -903,7 +903,7 @@ bool GeoIcons::RunMousePicking( const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, const common::Coordinate& /* mouseGeoCoords */, - std::shared_ptr& /* eventHandler */) + std::shared_ptr& eventHandler) { std::unique_lock lock {p->iconMutex_}; @@ -993,12 +993,27 @@ bool GeoIcons::RunMousePicking( if (it != p->currentHoverIcons_.crend()) { itemPicked = true; - util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + if (!it->di_->hoverText_.empty()) + { + // Show tooltip + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + if (it->di_->event_ != nullptr) + { + eventHandler = it->di_; + } } return itemPicked; } +void GeoIcons::RegisterEventHandler( + const std::shared_ptr& di, + const std::function& eventHandler) +{ + di->event_ = eventHandler; +} + } // namespace draw } // namespace gl } // namespace qt diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp index 4d819681..073fc118 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp @@ -183,6 +183,16 @@ public: */ void FinishIcons(); + /** + * Registers an event handler for an icon. + * + * @param [in] di Icon draw item + * @param [in] eventHandler Event handler function + */ + static void + RegisterEventHandler(const std::shared_ptr& di, + const std::function& eventHandler); + private: class Impl; diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 99208a10..fd7bee13 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -1,13 +1,17 @@ #include #include +#include #include +#include #include +#include #include #include #include #include #include +#include #include #include @@ -27,6 +31,10 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string kNameName_ = "name"; static const std::string kLatitudeName_ = "latitude"; static const std::string kLongitudeName_ = "longitude"; +static const std::string kIconName_ = "icon"; +static const std::string kIconColorName_ = "icon-color"; + +static const std::string defaultIconName = "images/location-marker"; class MarkerManager::Impl { @@ -36,15 +44,16 @@ public: explicit Impl(MarkerManager* self) : self_ {self} {} ~Impl() { threadPool_.join(); } - std::string markerSettingsPath_ {""}; - std::vector> markerRecords_ {}; + std::string markerSettingsPath_ {""}; + std::vector> markerRecords_ {}; std::unordered_map idToIndex_ {}; - + std::unordered_map markerIcons_ {}; MarkerManager* self_; boost::asio::thread_pool threadPool_ {1u}; std::shared_mutex markerRecordLock_ {}; + std::shared_mutex markerIconsLock_ {}; void InitializeMarkerSettings(); void ReadMarkerSettings(); @@ -53,16 +62,12 @@ public: void InitalizeIds(); types::MarkerId NewId(); - types::MarkerId lastId_; + types::MarkerId lastId_ {0}; }; class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(const std::string& name, double latitude, double longitude) : - markerInfo_ {types::MarkerInfo(name, latitude, longitude)} - { - } MarkerRecord(const types::MarkerInfo& info) : markerInfo_ {info} { @@ -81,16 +86,50 @@ public: { jv = {{kNameName_, record->markerInfo_.name}, {kLatitudeName_, record->markerInfo_.latitude}, - {kLongitudeName_, record->markerInfo_.longitude}}; + {kLongitudeName_, record->markerInfo_.longitude}, + {kIconName_, record->markerInfo_.iconName}, + {kIconColorName_, + util::color::ToArgbString(record->markerInfo_.iconColor)}}; } friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - return MarkerRecord( + static const boost::gil::rgba8_pixel_t defaultIconColor = + util::color::ToRgba8PixelT("#ffff0000"); + + const boost::json::object& jo = jv.as_object(); + + std::string iconName = defaultIconName; + boost::gil::rgba8_pixel_t iconColor = defaultIconColor; + + if (jo.contains(kIconName_) && jo.at(kIconName_).is_string()) + { + iconName = boost::json::value_to(jv.at(kIconName_)); + } + + if (jo.contains(kIconColorName_) && jo.at(kIconName_).is_string()) + { + try + { + iconColor = util::color::ToRgba8PixelT( + boost::json::value_to(jv.at(kIconColorName_))); + } + catch (const std::exception& ex) + { + logger_->warn( + "Could not parse color value in location-markers.json with the " + "following exception: {}", + ex.what()); + } + } + + return {types::MarkerInfo( boost::json::value_to(jv.at(kNameName_)), boost::json::value_to(jv.at(kLatitudeName_)), - boost::json::value_to(jv.at(kLongitudeName_))); + boost::json::value_to(jv.at(kLongitudeName_)), + iconName, + iconColor)}; } }; @@ -129,7 +168,7 @@ void MarkerManager::Impl::ReadMarkerSettings() boost::json::value markerJson = nullptr; { - std::unique_lock lock(markerRecordLock_); + const std::unique_lock lock(markerRecordLock_); // Determine if marker settings exists if (std::filesystem::exists(markerSettingsPath_)) @@ -147,18 +186,16 @@ void MarkerManager::Impl::ReadMarkerSettings() { try { - MarkerRecord record = - boost::json::value_to(markerEntry); + auto record = boost::json::value_to(markerEntry); - if (!record.markerInfo_.name.empty()) - { - types::MarkerId id = NewId(); - size_t index = markerRecords_.size(); - record.markerInfo_.id = id; - markerRecords_.emplace_back( - std::make_shared(record.markerInfo_)); - idToIndex_.emplace(id, index); - } + const types::MarkerId id = NewId(); + const size_t index = markerRecords_.size(); + record.markerInfo_.id = id; + markerRecords_.emplace_back( + std::make_shared(record.markerInfo_)); + idToIndex_.emplace(id, index); + + self_->add_icon(record.markerInfo_.iconName, true); } catch (const std::exception& ex) { @@ -166,6 +203,8 @@ void MarkerManager::Impl::ReadMarkerSettings() } } + ResourceManager::BuildAtlas(); + logger_->debug("{} location marker entries", markerRecords_.size()); } } @@ -177,7 +216,7 @@ void MarkerManager::Impl::WriteMarkerSettings() { logger_->info("Saving location marker settings"); - std::shared_lock lock(markerRecordLock_); + const std::shared_lock lock(markerRecordLock_); auto markerJson = boost::json::value_from(markerRecords_); util::json::WriteJsonFile(markerSettingsPath_, markerJson); } @@ -198,6 +237,20 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) MarkerManager::MarkerManager() : p(std::make_unique(this)) { + static const std::vector defaultMarkerIcons_ { + types::MarkerIconInfo(types::ImageTexture::LocationMarker, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationPin, 6, 16), + types::MarkerIconInfo(types::ImageTexture::LocationCrosshair, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationStar, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationBriefcase, -1, -1), + types::MarkerIconInfo( + types::ImageTexture::LocationBuildingColumns, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationBuilding, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationCaravan, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationHouse, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationTent, -1, -1), + }; + p->InitializeMarkerSettings(); boost::asio::post(p->threadPool_, @@ -207,8 +260,18 @@ MarkerManager::MarkerManager() : p(std::make_unique(this)) { // Read Marker settings on startup main::Application::WaitForInitialization(); + { + const std::unique_lock lock(p->markerIconsLock_); + p->markerIcons_.reserve( + defaultMarkerIcons_.size()); + for (auto& icon : defaultMarkerIcons_) + { + p->markerIcons_.emplace(icon.name, icon); + } + } p->ReadMarkerSettings(); + Q_EMIT IconsReady(); Q_EMIT MarkersInitialized(p->markerRecords_.size()); } catch (const std::exception& ex) @@ -230,7 +293,7 @@ size_t MarkerManager::marker_count() std::optional MarkerManager::get_marker(types::MarkerId id) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return {}; @@ -248,7 +311,7 @@ std::optional MarkerManager::get_marker(types::MarkerId id) std::optional MarkerManager::get_index(types::MarkerId id) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return {}; @@ -256,10 +319,11 @@ std::optional MarkerManager::get_index(types::MarkerId id) return p->idToIndex_[id]; } -void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& marker) +void MarkerManager::set_marker(types::MarkerId id, + const types::MarkerInfo& marker) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return; @@ -270,33 +334,39 @@ void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& mark logger_->warn("id in idToIndex_ but out of range!"); return; } - std::shared_ptr& markerRecord = + const std::shared_ptr& markerRecord = p->markerRecords_[index]; - markerRecord->markerInfo_ = marker; + markerRecord->markerInfo_ = marker; + markerRecord->markerInfo_.id = id; + + add_icon(marker.iconName); } Q_EMIT MarkerChanged(id); Q_EMIT MarkersUpdated(); } -void MarkerManager::add_marker(const types::MarkerInfo& marker) +types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) { types::MarkerId id; { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); id = p->NewId(); size_t index = p->markerRecords_.size(); p->idToIndex_.emplace(id, index); p->markerRecords_.emplace_back(std::make_shared(marker)); p->markerRecords_[index]->markerInfo_.id = id; + + add_icon(marker.iconName); } Q_EMIT MarkerAdded(id); Q_EMIT MarkersUpdated(); + return id; } void MarkerManager::remove_marker(types::MarkerId id) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return; @@ -327,7 +397,7 @@ void MarkerManager::remove_marker(types::MarkerId id) void MarkerManager::move_marker(size_t from, size_t to) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (from >= p->markerRecords_.size() || to >= p->markerRecords_.size()) { return; @@ -358,13 +428,64 @@ void MarkerManager::move_marker(size_t from, size_t to) void MarkerManager::for_each(std::function func) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); for (auto marker : p->markerRecords_) { func(marker->markerInfo_); } } +void MarkerManager::add_icon(const std::string& name, bool startup) +{ + { + const std::unique_lock lock(p->markerIconsLock_); + if (p->markerIcons_.contains(name)) + { + return; + } + const std::shared_ptr image = + ResourceManager::LoadImageResource(name); + + if (image) + { + auto icon = types::MarkerIconInfo(name, -1, -1, image); + p->markerIcons_.emplace(name, icon); + } + else + { + // defaultIconName should always be in markerIcons, so at is fine + auto icon = p->markerIcons_.at(defaultIconName); + p->markerIcons_.emplace(name, icon); + } + } + + if (!startup) + { + ResourceManager::BuildAtlas(); + Q_EMIT IconAdded(name); + } +} + +std::optional +MarkerManager::get_icon(const std::string& name) +{ + const std::shared_lock lock(p->markerIconsLock_); + auto it = p->markerIcons_.find(name); + if (it != p->markerIcons_.end()) + { + return it->second; + } + + return {}; +} + +const std::unordered_map +MarkerManager::get_icons() +{ + const std::shared_lock lock(p->markerIconsLock_); + return p->markerIcons_; +} + // Only use for testing void MarkerManager::set_marker_settings_path(const std::string& path) { @@ -377,7 +498,7 @@ std::shared_ptr MarkerManager::Instance() static std::weak_ptr markerManagerReference_ {}; static std::mutex instanceMutex_ {}; - std::unique_lock lock(instanceMutex_); + const std::unique_lock lock(instanceMutex_); std::shared_ptr markerManager = markerManagerReference_.lock(); @@ -391,6 +512,11 @@ std::shared_ptr MarkerManager::Instance() return markerManager; } +const std::string& MarkerManager::getDefaultIconName() +{ + return defaultIconName; +} + } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 37ec1c31..7004e117 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -23,11 +23,15 @@ public: size_t marker_count(); std::optional get_marker(types::MarkerId id); - std::optional get_index(types::MarkerId id); + std::optional get_index(types::MarkerId id); void set_marker(types::MarkerId id, const types::MarkerInfo& marker); - void add_marker(const types::MarkerInfo& marker); - void remove_marker(types::MarkerId id); - void move_marker(size_t from, size_t to); + types::MarkerId add_marker(const types::MarkerInfo& marker); + void remove_marker(types::MarkerId id); + void move_marker(size_t from, size_t to); + + void add_icon(const std::string& name, bool startup = false); + std::optional get_icon(const std::string& name); + const std::unordered_map get_icons(); void for_each(std::function func); @@ -35,6 +39,7 @@ public: void set_marker_settings_path(const std::string& path); static std::shared_ptr Instance(); + static const std::string& getDefaultIconName(); signals: void MarkersInitialized(size_t count); @@ -43,6 +48,9 @@ signals: void MarkerAdded(types::MarkerId id); void MarkerRemoved(types::MarkerId id); + void IconsReady(); + void IconAdded(std::string name); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 443d771b..c6c40a26 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -22,6 +22,9 @@ namespace ResourceManager static const std::string logPrefix_ = "scwx::qt::manager::resource_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static const size_t atlasWidth = 2048; +static const size_t atlasHeight = 2048; + static void LoadFonts(); static void LoadTextures(); @@ -68,8 +71,7 @@ LoadImageResources(const std::vector& urlStrings) if (!images.empty()) { - util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); + BuildAtlas(); } return images; @@ -103,7 +105,13 @@ static void LoadTextures() GetTexturePath(lineTexture)); } - textureAtlas.BuildAtlas(2048, 2048); + BuildAtlas(); +} + +void BuildAtlas() +{ + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.BuildAtlas(atlasWidth, atlasHeight); } } // namespace ResourceManager diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index 00658891..ec7cf65e 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -22,6 +22,7 @@ std::shared_ptr LoadImageResource(const std::string& urlString); std::vector> LoadImageResources(const std::vector& urlStrings); +void BuildAtlas(); } // namespace ResourceManager } // namespace manager diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index bfe0908c..f7ba99ae 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -9,10 +9,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -127,8 +128,6 @@ public: ImGui_ImplQt_Init(); InitializeCustomStyles(); - - ConnectSignals(); } ~MapWidgetImpl() @@ -219,6 +218,8 @@ public: std::shared_ptr layerModel_ { model::LayerModel::Instance()}; + ui::EditMarkerDialog* editMarkerDialog_ {nullptr}; + std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; std::shared_ptr placefileManager_ { @@ -283,6 +284,12 @@ MapWidget::MapWidget(std::size_t id, const QMapLibre::Settings& settings) : setFocusPolicy(Qt::StrongFocus); ImGui_ImplQt_RegisterWidget(this); + + // Qt parent deals with memory management + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + p->editMarkerDialog_ = new ui::EditMarkerDialog(this); + + p->ConnectSignals(); } MapWidget::~MapWidget() @@ -429,6 +436,16 @@ void MapWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat) switch (hotkey) { + case types::Hotkey::AddLocationMarker: + if (hasMouse_) + { + auto coordinate = map_->coordinateForPixel(lastPos_); + + editMarkerDialog_->setup(coordinate.first, coordinate.second); + editMarkerDialog_->show(); + } + break; + case types::Hotkey::ChangeMapStyle: if (context_->settings().isActive_) { diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 0480a63e..906ef7c1 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -1,9 +1,14 @@ #include #include #include -#include -#include #include +#include +#include + +#include +#include + +#include namespace scwx { @@ -19,48 +24,104 @@ class MarkerLayer::Impl { public: explicit Impl(MarkerLayer* self, std::shared_ptr context) : - self_ {self}, geoIcons_ {std::make_shared(context)} + self_ {self}, + geoIcons_ {std::make_shared(context)}, + editMarkerDialog_ {std::make_shared()} { ConnectSignals(); } - ~Impl() {} + ~Impl() = default; void ReloadMarkers(); void ConnectSignals(); + std::shared_ptr markerManager_ { + manager::MarkerManager::Instance()}; + + void set_icon_sheets(); + MarkerLayer* self_; - const std::string& markerIconName_ { - types::GetTextureName(types::ImageTexture::LocationMarker)}; std::shared_ptr geoIcons_; + std::shared_ptr editMarkerDialog_; }; void MarkerLayer::Impl::ConnectSignals() { - auto markerManager = manager::MarkerManager::Instance(); - - QObject::connect(markerManager.get(), - &manager::MarkerManager::MarkersUpdated, - self_, - [this]() - { - this->ReloadMarkers(); - }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::MarkersUpdated, + self_, + [this]() { ReloadMarkers(); }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::IconsReady, + self_, + [this]() { set_icon_sheets(); }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::IconAdded, + self_, + [this]() { set_icon_sheets(); }); } void MarkerLayer::Impl::ReloadMarkers() { logger_->debug("ReloadMarkers()"); - auto markerManager = manager::MarkerManager::Instance(); geoIcons_->StartIcons(); - - markerManager->for_each( + markerManager_->for_each( [this](const types::MarkerInfo& marker) { - std::shared_ptr icon = geoIcons_->AddIcon(); - geoIcons_->SetIconTexture(icon, markerIconName_, 0); + // must use local ID, instead of reference to marker in event handler + // callback. + const types::MarkerId id = marker.id; + + const std::shared_ptr icon = + geoIcons_->AddIcon(); + + const std::string latitudeString = + common::GetLatitudeString(marker.latitude); + const std::string longitudeString = + common::GetLongitudeString(marker.longitude); + + const std::string hoverText = + marker.name != "" ? + fmt::format( + "{}\n{}, {}", marker.name, latitudeString, longitudeString) : + fmt::format("{}, {}", latitudeString, longitudeString); + + auto iconInfo = markerManager_->get_icon(marker.iconName); + if (iconInfo) + { + geoIcons_->SetIconTexture(icon, iconInfo->name, 0); + } + else + { + geoIcons_->SetIconTexture(icon, marker.iconName, 0); + } + geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); + geoIcons_->SetIconHoverText(icon, hoverText); + geoIcons_->SetIconModulate(icon, marker.iconColor); + geoIcons_->RegisterEventHandler( + icon, + [this, id](QEvent* ev) + { + switch (ev->type()) + { + case QEvent::Type::MouseButtonPress: + { + auto* mouseEvent = reinterpret_cast(ev); + if (mouseEvent->buttons() == Qt::MouseButton::RightButton) + { + editMarkerDialog_->setup(id); + editMarkerDialog_->show(); + } + } + break; + + default: + break; + } + }); }); geoIcons_->FinishIcons(); @@ -80,17 +141,28 @@ void MarkerLayer::Initialize() logger_->debug("Initialize()"); DrawLayer::Initialize(); - p->geoIcons_->StartIconSheets(); - p->geoIcons_->AddIconSheet(p->markerIconName_); - p->geoIcons_->FinishIconSheets(); - + p->set_icon_sheets(); p->ReloadMarkers(); } +void MarkerLayer::Impl::set_icon_sheets() +{ + geoIcons_->StartIconSheets(); + for (auto& markerIcon : markerManager_->get_icons()) + { + geoIcons_->AddIconSheet(markerIcon.second.name, + 0, + 0, + markerIcon.second.hotX, + markerIcon.second.hotY); + } + geoIcons_->FinishIconSheets(); +} + void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - // auto markerManager = manager::MarkerManager::Instance(); gl::OpenGLFunctions& gl = context()->gl(); + context()->set_render_parameters(params); DrawLayer::Render(params); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index e550312e..77fb7ab7 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ namespace model static const std::string logPrefix_ = "scwx::qt::model::marker_model"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static const int iconSize_ = 30; static constexpr int kFirstColumn = static_cast(MarkerModel::Column::Latitude); @@ -38,7 +40,6 @@ public: MarkerModel::MarkerModel(QObject* parent) : QAbstractTableModel(parent), p(std::make_unique()) { - connect(p->markerManager_.get(), &manager::MarkerManager::MarkersInitialized, this, @@ -78,26 +79,11 @@ Qt::ItemFlags MarkerModel::flags(const QModelIndex& index) const { Qt::ItemFlags flags = QAbstractTableModel::flags(index); - switch (index.column()) - { - case static_cast(Column::Name): - case static_cast(Column::Latitude): - case static_cast(Column::Longitude): - flags |= Qt::ItemFlag::ItemIsEditable; - break; - default: - break; - } - return flags; } QVariant MarkerModel::data(const QModelIndex& index, int role) const { - - static const char COORDINATE_FORMAT = 'g'; - static const int COORDINATE_PRECISION = 10; - if (!index.isValid() || index.row() < 0 || static_cast(index.row()) >= p->markerIds_.size()) { @@ -118,8 +104,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const { case static_cast(Column::Name): if (role == Qt::ItemDataRole::DisplayRole || - role == Qt::ItemDataRole::ToolTipRole || - role == Qt::ItemDataRole::EditRole) + role == Qt::ItemDataRole::ToolTipRole) { return QString::fromStdString(markerInfo->name); } @@ -132,11 +117,6 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QString::fromStdString( common::GetLatitudeString(markerInfo->latitude)); } - else if (role == Qt::ItemDataRole::EditRole) - { - return QString::number( - markerInfo->latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); - } break; case static_cast(Column::Longitude): @@ -146,13 +126,41 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QString::fromStdString( common::GetLongitudeString(markerInfo->longitude)); } - else if (role == Qt::ItemDataRole::EditRole) - { - return QString::number( - markerInfo->longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); - } break; break; + case static_cast(Column::Icon): + if (role == Qt::ItemDataRole::DisplayRole) + { + std::optional icon = + p->markerManager_->get_icon(markerInfo->iconName); + if (icon) + { + return QString::fromStdString(icon->shortName); + } + else + { + return {}; + } + } + else if (role == Qt::ItemDataRole::DecorationRole) + { + std::optional icon = + p->markerManager_->get_icon(markerInfo->iconName); + if (icon) + { + return util::modulateColors(icon->qIcon, + QSize(iconSize_, iconSize_), + QColor(markerInfo->iconColor[0], + markerInfo->iconColor[1], + markerInfo->iconColor[2], + markerInfo->iconColor[3])); + } + else + { + return {}; + } + } + break; default: break; @@ -190,6 +198,9 @@ QVariant MarkerModel::headerData(int section, case static_cast(Column::Longitude): return tr("Longitude"); + case static_cast(Column::Icon): + return tr("Icon"); + default: break; } @@ -199,78 +210,9 @@ QVariant MarkerModel::headerData(int section, return QVariant(); } -bool MarkerModel::setData(const QModelIndex& index, - const QVariant& value, - int role) +bool MarkerModel::setData(const QModelIndex&, const QVariant&, int) { - - if (!index.isValid() || index.row() < 0 || - static_cast(index.row()) >= p->markerIds_.size()) - { - return false; - } - - types::MarkerId id = p->markerIds_[index.row()]; - std::optional markerInfo = - p->markerManager_->get_marker(id); - if (!markerInfo) - { - return false; - } - bool result = false; - - switch(index.column()) - { - case static_cast(Column::Name): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - markerInfo->name = str.toStdString(); - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - break; - - case static_cast(Column::Latitude): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - bool ok; - double latitude = str.toDouble(&ok); - if (!str.isEmpty() && ok && -90 <= latitude && latitude <= 90) - { - markerInfo->latitude = latitude; - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - } - break; - - case static_cast(Column::Longitude): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - bool ok; - double longitude = str.toDouble(&ok); - if (!str.isEmpty() && ok && -180 <= longitude && longitude <= 180) - { - markerInfo->longitude = longitude; - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - } - break; - - default: - break; - } - - if (result) - { - Q_EMIT dataChanged(index, index); - } - - return result; + return false; } void MarkerModel::HandleMarkersInitialized(size_t count) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index 4fc6c95c..91c8854f 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -17,7 +17,8 @@ public: { Latitude = 0, Longitude = 1, - Name = 2, + Icon = 2, + Name = 3, }; explicit MarkerModel(QObject* parent = nullptr); diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp index 0edaf840..e9c6b007 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp @@ -12,6 +12,7 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::hotkey_settings"; static const std::unordered_map kDefaultHotkeys_ { + {types::Hotkey::AddLocationMarker, QKeySequence {Qt::Key::Key_M}}, {types::Hotkey::ChangeMapStyle, QKeySequence {Qt::Key::Key_Z}}, {types::Hotkey::CopyCursorCoordinates, QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp index 8a4d0ee5..72c77f63 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp @@ -13,6 +13,7 @@ namespace types { static const std::unordered_map hotkeyShortName_ { + {Hotkey::AddLocationMarker, "add_location_marker"}, {Hotkey::ChangeMapStyle, "change_map_style"}, {Hotkey::CopyCursorCoordinates, "copy_cursor_coordinates"}, {Hotkey::CopyMapCoordinates, "copy_map_coordinates"}, @@ -52,6 +53,7 @@ static const std::unordered_map hotkeyShortName_ { {Hotkey::Unknown, "?"}}; static const std::unordered_map hotkeyLongName_ { + {Hotkey::AddLocationMarker, "Add Location Marker"}, {Hotkey::ChangeMapStyle, "Change Map Style"}, {Hotkey::CopyCursorCoordinates, "Copy Cursor Coordinates"}, {Hotkey::CopyMapCoordinates, "Copy Map Coordinates"}, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp index c2118a4f..2107a009 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp @@ -13,6 +13,7 @@ namespace types enum class Hotkey { + AddLocationMarker, ChangeMapStyle, CopyCursorCoordinates, CopyMapCoordinates, @@ -52,7 +53,7 @@ enum class Hotkey Unknown }; typedef scwx::util:: - Iterator + Iterator HotkeyIterator; Hotkey GetHotkeyFromShortName(const std::string& name); diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index e3d28e26..661aa207 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -1,29 +1,82 @@ #pragma once -#include -#include +#include -namespace scwx +#include +#include +#include + +#include +#include +#include + +namespace scwx::qt::types { -namespace qt -{ -namespace types -{ -typedef std::uint64_t MarkerId; +using MarkerId = std::uint64_t; struct MarkerInfo { - MarkerInfo(const std::string& name, double latitude, double longitude) : - name {name}, latitude {latitude}, longitude {longitude} + MarkerInfo(std::string name, + double latitude, + double longitude, + std::string iconName, + const boost::gil::rgba8_pixel_t& iconColor) : + name {std::move(name)}, + latitude {latitude}, + longitude {longitude}, + iconName {std::move(iconName)}, + iconColor {iconColor} { } - MarkerId id; - std::string name; - double latitude; - double longitude; + MarkerId id {0}; + std::string name; + double latitude; + double longitude; + std::string iconName; + boost::gil::rgba8_pixel_t iconColor; }; -} // namespace types -} // namespace qt -} // namespace scwx +struct MarkerIconInfo +{ + // Initializer for default icons (which use a texture) + explicit MarkerIconInfo(types::ImageTexture texture, + std::int32_t hotX, + std::int32_t hotY) : + name {types::GetTextureName(texture)}, + path {types::GetTexturePath(texture)}, + hotX {hotX}, + hotY {hotY}, + qIcon {QIcon(QString::fromStdString(path))}, + image {} + { + auto qName = QString::fromStdString(name); + QStringList parts = qName.split("location-"); + shortName = parts.last().toStdString(); + } + + // Initializer for custom icons (which use a file path) + explicit MarkerIconInfo(const std::string& path, + std::int32_t hotX, + std::int32_t hotY, + std::shared_ptr image) : + name {path}, + path {path}, + shortName {QFileInfo(path.c_str()).fileName().toStdString()}, + hotX {hotX}, + hotY {hotY}, + qIcon {QIcon(QString::fromStdString(path))}, + image {image} + { + } + + std::string name; + std::string path; + std::string shortName; + std::int32_t hotX; + std::int32_t hotY; + QIcon qIcon; + std::optional> image; +}; + +} // namespace scwx::qt::types diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp index 7f0c7a24..18efd9b9 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.cpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -25,8 +25,33 @@ static const std::unordered_map imageTextureInfo_ { {ImageTexture::Cursor17, {"images/cursor-17", ":/res/textures/images/cursor-17.png"}}, {ImageTexture::Dot3, {"images/dot-3", ":/res/textures/images/dot-3.png"}}, + {ImageTexture::LocationBriefcase, + {"images/location-briefcase", + ":/res/icons/font-awesome-6/briefcase-solid.svg"}}, + {ImageTexture::LocationBuildingColumns, + {"images/location-building-columns", + ":/res/icons/font-awesome-6/building-columns-solid.svg"}}, + {ImageTexture::LocationBuilding, + {"images/location-building", + ":/res/icons/font-awesome-6/building-solid.svg"}}, + {ImageTexture::LocationCaravan, + {"images/location-caravan", + ":/res/icons/font-awesome-6/caravan-solid.svg"}}, + {ImageTexture::LocationCrosshair, + {"images/location-crosshair", + ":/res/icons/font-awesome-6/location-crosshairs-solid.svg"}}, + {ImageTexture::LocationHouse, + {"images/location-house", + ":/res/icons/font-awesome-6/house-solid-white.svg"}}, {ImageTexture::LocationMarker, {"images/location-marker", ":/res/textures/images/location-marker.svg"}}, + {ImageTexture::LocationPin, + {"images/location-pin", ":/res/icons/font-awesome-6/location-pin.svg"}}, + {ImageTexture::LocationStar, + {"images/location-star", + ":/res/icons/font-awesome-6/star-solid-white.svg"}}, + {ImageTexture::LocationTent, + {"images/location-tent", ":/res/icons/font-awesome-6/tent-solid.svg"}}, {ImageTexture::MapboxLogo, {"images/mapbox-logo", ":/res/textures/images/mapbox-logo.svg"}}, {ImageTexture::MapTilerLogo, diff --git a/scwx-qt/source/scwx/qt/types/texture_types.hpp b/scwx-qt/source/scwx/qt/types/texture_types.hpp index 307a7638..d5eabc4a 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.hpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.hpp @@ -18,7 +18,16 @@ enum class ImageTexture Crosshairs24, Cursor17, Dot3, + LocationBriefcase, + LocationBuildingColumns, + LocationBuilding, + LocationCaravan, + LocationCrosshair, + LocationHouse, LocationMarker, + LocationPin, + LocationStar, + LocationTent, MapboxLogo, MapTilerLogo }; diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp new file mode 100644 index 00000000..a6aaa4d4 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -0,0 +1,314 @@ +#include "edit_marker_dialog.hpp" +#include "ui_edit_marker_dialog.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace scwx::qt::ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::edit_marker_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class EditMarkerDialog::Impl +{ +public: + explicit Impl(EditMarkerDialog* self) : self_ {self} {} + + void show_color_dialog(); + void show_icon_file_dialog(); + + void set_icon_color(const std::string& color); + + void connect_signals(); + + void handle_accepted(); + void handle_rejected(); + + EditMarkerDialog* self_; + QPushButton* deleteButton_ {nullptr}; + QIcon get_colored_icon(const types::MarkerIconInfo& marker, + const std::string& color); + + std::shared_ptr markerManager_ = + manager::MarkerManager::Instance(); + types::MarkerId editId_ {0}; + bool adding_ {false}; + std::string setIconOnAdded_ {""}; +}; + +QIcon EditMarkerDialog::Impl::get_colored_icon( + const types::MarkerIconInfo& marker, const std::string& color) +{ + return util::modulateColors(marker.qIcon, + self_->ui->iconComboBox->iconSize(), + QColor(QString::fromStdString(color))); +} + +EditMarkerDialog::EditMarkerDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::EditMarkerDialog) +{ + ui->setupUi(this); + + for (auto& markerIcon : p->markerManager_->get_icons()) + { + ui->iconComboBox->addItem( + markerIcon.second.qIcon, + QString::fromStdString(markerIcon.second.shortName), + QString::fromStdString(markerIcon.second.name)); + } + p->deleteButton_ = + ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); + p->connect_signals(); +} + +EditMarkerDialog::~EditMarkerDialog() +{ + delete ui; +} + +void EditMarkerDialog::setup() +{ + setup(0, 0); +} + +void EditMarkerDialog::setup(double latitude, double longitude) +{ + // By default use foreground color as marker color, mainly so the icons + // are vissable in the dropdown menu. + const QColor color = QWidget::palette().color(QWidget::foregroundRole()); + p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( + "", + latitude, + longitude, + manager::MarkerManager::getDefaultIconName(), + boost::gil::rgba8_pixel_t {static_cast(color.red()), + static_cast(color.green()), + static_cast(color.blue()), + static_cast(color.alpha())})); + + setup(p->editId_); + p->adding_ = true; +} + +void EditMarkerDialog::setup(types::MarkerId id) +{ + std::optional marker = p->markerManager_->get_marker(id); + if (!marker) + { + return; + } + + p->editId_ = id; + p->adding_ = false; + + const std::string iconColorStr = + util::color::ToArgbString(marker->iconColor); + p->set_icon_color(iconColorStr); + + int iconIndex = + ui->iconComboBox->findData(QString::fromStdString(marker->iconName)); + if (iconIndex < 0 || marker->iconName == "") + { + iconIndex = 0; + } + + ui->nameLineEdit->setText(QString::fromStdString(marker->name)); + ui->iconComboBox->setCurrentIndex(iconIndex); + ui->latitudeDoubleSpinBox->setValue(marker->latitude); + ui->longitudeDoubleSpinBox->setValue(marker->longitude); + ui->iconColorLineEdit->setText(QString::fromStdString(iconColorStr)); +} + +types::MarkerInfo EditMarkerDialog::get_marker_info() const +{ + const QString colorName = ui->iconColorLineEdit->text(); + const boost::gil::rgba8_pixel_t color = + util::color::ToRgba8PixelT(colorName.toStdString()); + + return types::MarkerInfo( + ui->nameLineEdit->text().toStdString(), + ui->latitudeDoubleSpinBox->value(), + ui->longitudeDoubleSpinBox->value(), + ui->iconComboBox->currentData().toString().toStdString(), + color); +} + +void EditMarkerDialog::Impl::show_color_dialog() +{ + // WA_DeleteOnClose manages memory + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto* dialog = new QColorDialog(self_); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); + + const QColor initialColor(self_->ui->iconColorLineEdit->text()); + if (initialColor.isValid()) + { + dialog->setCurrentColor(initialColor); + } + + QObject::connect(dialog, + &QColorDialog::colorSelected, + self_, + [this](const QColor& qColor) + { + const QString colorName = + qColor.name(QColor::NameFormat::HexArgb); + self_->ui->iconColorLineEdit->setText(colorName); + set_icon_color(colorName.toStdString()); + }); + dialog->open(); +} + +void EditMarkerDialog::Impl::show_icon_file_dialog() +{ + auto* dialog = new QFileDialog(self_); + + dialog->setFileMode(QFileDialog::ExistingFile); + dialog->setNameFilters({"Icon (*.png *.svg)", "All Files (*)"}); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + QObject::connect(dialog, + &QFileDialog::fileSelected, + self_, + [this](const QString& file) + { + const std::string path = + QDir::toNativeSeparators(file).toStdString(); + setIconOnAdded_ = path; + markerManager_->add_icon(path); + }); + dialog->open(); +} + +void EditMarkerDialog::Impl::connect_signals() +{ + connect(self_, + &EditMarkerDialog::accepted, + self_, + [this]() { handle_accepted(); }); + + connect(self_, + &EditMarkerDialog::rejected, + self_, + [this]() { handle_rejected(); }); + + connect(deleteButton_, + &QPushButton::clicked, + self_, + [this]() + { + markerManager_->remove_marker(editId_); + self_->done(0); + }); + + connect(self_->ui->iconColorLineEdit, + &QLineEdit::textEdited, + self_, + [this](const QString& text) { set_icon_color(text.toStdString()); }); + + connect(self_->ui->iconColorButton, + &QAbstractButton::clicked, + self_, + [this]() { show_color_dialog(); }); + + connect(self_->ui->iconFileOpenButton, + &QPushButton::clicked, + self_, + [this]() { show_icon_file_dialog(); }); + + connect(markerManager_.get(), + &manager::MarkerManager::IconAdded, + self_, + [this]() + { + const std::string color = + self_->ui->iconColorLineEdit->text().toStdString(); + set_icon_color(color); + + if (setIconOnAdded_ != "") + { + const int i = self_->ui->iconComboBox->findData( + QString::fromStdString(setIconOnAdded_)); + if (i >= 0) + { + self_->ui->iconComboBox->setCurrentIndex(i); + setIconOnAdded_ = ""; + } + } + }); + + connect(self_->ui->buttonBox->button(QDialogButtonBox::Apply), + &QAbstractButton::clicked, + self_, + [this]() { handle_accepted(); }); +} + +void EditMarkerDialog::Impl::set_icon_color(const std::string& color) +{ + self_->ui->iconColorFrame->setStyleSheet( + QString::fromStdString(fmt::format("background-color: {}", color))); + + auto* iconComboBox = self_->ui->iconComboBox; + + const QVariant currentIcon = iconComboBox->currentData(); + + self_->ui->iconComboBox->clear(); + for (auto& markerIcon : markerManager_->get_icons()) + { + const int i = + iconComboBox->findData(QString::fromStdString(markerIcon.second.name)); + const QIcon icon = get_colored_icon(markerIcon.second, color); + if (i < 0) + { + iconComboBox->addItem( + icon, + QString::fromStdString(markerIcon.second.shortName), + QString::fromStdString(markerIcon.second.name)); + } + else + { + self_->ui->iconComboBox->setItemIcon(i, icon); + } + } + + const int i = iconComboBox->findData(currentIcon); + if (i < 0) + { + return; + } + + iconComboBox->setCurrentIndex(i); +} + +void EditMarkerDialog::Impl::handle_accepted() +{ + markerManager_->set_marker(editId_, self_->get_marker_info()); +} + +void EditMarkerDialog::Impl::handle_rejected() +{ + if (adding_) + { + markerManager_->remove_marker(editId_); + } +} + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp new file mode 100644 index 00000000..c20ebe27 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -0,0 +1,34 @@ +#pragma once +#include + +#include + +namespace Ui +{ +class EditMarkerDialog; +} + +namespace scwx::qt::ui +{ +class EditMarkerDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(EditMarkerDialog) + +public: + explicit EditMarkerDialog(QWidget* parent = nullptr); + ~EditMarkerDialog() override; + + void setup(); + void setup(double latitude, double longitude); + void setup(types::MarkerId id); + + [[nodiscard]] types::MarkerInfo get_marker_info() const; + +private: + class Impl; + std::unique_ptr p; + Ui::EditMarkerDialog* ui; +}; + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui new file mode 100644 index 00000000..d3d47500 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui @@ -0,0 +1,210 @@ + + + EditMarkerDialog + + + + 0 + 0 + 400 + 249 + + + + Edit Location Marker + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + 24 + 24 + + + + QFrame::Shape::Box + + + QFrame::Shadow::Plain + + + + + + + #ffffffff + + + + + + + ... + + + + :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + + + + + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + 5 + + + -90.000000000000000 + + + 90.000000000000000 + + + + + + + Name + + + + + + + Icon + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + Longitude + + + + + + + Icon Color + + + + + + + Latitude + + + + + + + Add Custom Icon + + + ... + + + + + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + 5 + + + -180.000000000000000 + + + 180.000000000000000 + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + + buttonBox + accepted() + EditMarkerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditMarkerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index 0c2bc614..b3ba8440 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -23,17 +23,24 @@ class MarkerSettingsWidgetImpl { public: explicit MarkerSettingsWidgetImpl(MarkerSettingsWidget* self) : - self_ {self}, - markerModel_ {new model::MarkerModel(self_)} + self_ {self}, + markerModel_ {new model::MarkerModel(self_)}, + proxyModel_ {new QSortFilterProxyModel(self_)} { + proxyModel_->setSourceModel(markerModel_); + proxyModel_->setSortRole(Qt::DisplayRole); // TODO types::SortRole + proxyModel_->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel_->setFilterKeyColumn(-1); } void ConnectSignals(); - MarkerSettingsWidget* self_; - model::MarkerModel* markerModel_; + MarkerSettingsWidget* self_; + model::MarkerModel* markerModel_; + QSortFilterProxyModel* proxyModel_; std::shared_ptr markerManager_ { manager::MarkerManager::Instance()}; + std::shared_ptr editMarkerDialog_ {nullptr}; }; @@ -45,8 +52,9 @@ MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : ui->setupUi(this); ui->removeButton->setEnabled(false); + ui->markerView->setModel(p->proxyModel_); - ui->markerView->setModel(p->markerModel_); + p->editMarkerDialog_ = std::make_shared(this); p->ConnectSignals(); } @@ -63,7 +71,8 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this]() { - markerManager_->add_marker(types::MarkerInfo("", 0, 0)); + editMarkerDialog_->setup(); + editMarkerDialog_->show(); }); QObject::connect( self_->ui->removeButton, @@ -99,9 +108,30 @@ void MarkerSettingsWidgetImpl::ConnectSignals() return; } - bool itemSelected = selected.size() > 0; + const bool itemSelected = selected.size() > 0; self_->ui->removeButton->setEnabled(itemSelected); }); + QObject::connect(self_->ui->markerView, + &QAbstractItemView::doubleClicked, + self_, + [this](const QModelIndex& index) + { + const int row = index.row(); + if (row < 0) + { + return; + } + + std::optional id = + markerModel_->getId(row); + if (!id) + { + return; + } + + editMarkerDialog_->setup(*id); + editMarkerDialog_->show(); + }); } } // namespace ui diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp new file mode 100644 index 00000000..c205ce88 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp @@ -0,0 +1,64 @@ +#include + +#include +#include +#include +#include +#include + +namespace scwx::qt::util +{ + +void modulateColors_(QImage& image, const QColor& color) +{ + for (int y = 0; y < image.height(); ++y) + { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < image.width(); ++x) + { + // This is pulled from Qt Documentation + // https://doc.qt.io/qt-6/qimage.html#scanLine + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + QRgb& rgb = line[x]; + /* clang-format off + * NOLINTBEGIN(cppcoreguidelines-narrowing-conversions, bugprone-narrowing-conversions) + * qRed/qGreen/qBlue/qAlpha return values 0-255, handlable by float + * redF/greenF/blueF/alphaF are all 0-1, so output is 0-255 + * Rounding is fine for this. + * clang-format on + */ + const int red = qRed(rgb) * color.redF(); + const int green = qGreen(rgb) * color.greenF(); + const int blue = qBlue(rgb) * color.blueF(); + const int alpha = qAlpha(rgb) * color.alphaF(); + /* clang-format off + * NOLINTEND(cppcoreguidelines-narrowing-conversions, bugprone-narrowing-conversions) + * clang-format on + */ + + rgb = qRgba(red, green, blue, alpha); + } + } +} + +QImage modulateColors(const QImage& image, const QColor& color) +{ + QImage copy = image.copy(); + modulateColors_(copy, color); + return copy; +} + +QPixmap modulateColors(const QPixmap& pixmap, const QColor& color) +{ + QImage image = pixmap.toImage(); + modulateColors_(image, color); + return QPixmap::fromImage(image); +} + +QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color) +{ + const QPixmap pixmap = modulateColors(icon.pixmap(size), color); + return QIcon(pixmap); +} + +} // namespace scwx::qt::util diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp new file mode 100644 index 00000000..35326293 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace scwx::qt::util +{ + +QImage modulateColors(const QImage& image, const QColor& color); +QPixmap modulateColors(const QPixmap& pixmap, const QColor& color); +QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color); + +} // namespace scwx::qt::util diff --git a/test/data b/test/data index 4b4d9c54..0d085b1d 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 4b4d9c54b8218aa2297dbd457e3747091570f0d2 +Subproject commit 0d085b1df59045e14ca996982b4907b1a0da4fdb diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp index 700ffc6e..74ad28a9 100644 --- a/test/source/scwx/qt/model/marker_model.test.cpp +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -9,7 +10,6 @@ #include #include - namespace scwx { namespace qt @@ -25,11 +25,17 @@ static const std::string ONE_MARKERS_FILE = std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-one.json"; static const std::string FIVE_MARKERS_FILE = std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-five.json"; +static const std::string PART1_MARKER_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-part1.json"; static std::mutex initializedMutex {}; static std::condition_variable initializedCond {}; static bool initialized; +static const boost::gil::rgba8_pixel_t defaultIconColor = + util::color::ToRgba8PixelT("#ffff0000"); +static const std::string defaultIconName = "images/location-marker"; + void CompareFiles(const std::string& file1, const std::string& file2) { std::ifstream ifs1 {file1}; @@ -49,8 +55,8 @@ void CopyFile(const std::string& from, const std::string& to) CompareFiles(from, to); } -typedef void TestFunction(std::shared_ptr manager, - MarkerModel& model); +using TestFunction = void(std::shared_ptr, + MarkerModel&); void RunTest(const std::string& filename, TestFunction testFunction) { @@ -65,7 +71,7 @@ void RunTest(const std::string& filename, TestFunction testFunction) initialized = false; QObject::connect(manager.get(), &manager::MarkerManager::MarkersInitialized, - [](size_t count) + []() { std::unique_lock lock(initializedMutex); initialized = true; @@ -119,7 +125,10 @@ TEST(MarkerModelTest, AddRemove) RunTest(ONE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) - { manager->add_marker(types::MarkerInfo("Null", 0, 0)); }); + { + manager->add_marker(types::MarkerInfo( + "Null", 0, 0, defaultIconName, defaultIconColor)); + }); RunTest( EMPTY_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel& model) @@ -143,11 +152,16 @@ TEST(MarkerModelTest, AddFive) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("Null", 0, 0)); - manager->add_marker(types::MarkerInfo("North", 90, 0)); - manager->add_marker(types::MarkerInfo("South", -90, 0)); - manager->add_marker(types::MarkerInfo("East", 0, 90)); - manager->add_marker(types::MarkerInfo("West", 0, -90)); + manager->add_marker(types::MarkerInfo( + "Null", 0, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -161,10 +175,14 @@ TEST(MarkerModelTest, AddFour) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("North", 90, 0)); - manager->add_marker(types::MarkerInfo("South", -90, 0)); - manager->add_marker(types::MarkerInfo("East", 0, 90)); - manager->add_marker(types::MarkerInfo("West", 0, -90)); + manager->add_marker(types::MarkerInfo( + "North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -235,6 +253,17 @@ TEST(MarkerModelTest, RemoveFour) EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); } +TEST(MarkerModelTest, UpdateFromPart1) +{ + CopyFile(PART1_MARKER_FILE, TEMP_MARKERS_FILE); + + RunTest(ONE_MARKERS_FILE, + [](std::shared_ptr, MarkerModel&) {}); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + } // namespace model } // namespace qt } // namespace scwx